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
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PatientHttp
|
|
4
|
+
module Sidekiq
|
|
5
|
+
# Stores processor statistics in Redis with automatic expiration.
|
|
6
|
+
#
|
|
7
|
+
# This singleton class tracks various metrics about async HTTP requests,
|
|
8
|
+
# including total requests, errors, refused requests, and current inflight counts
|
|
9
|
+
# across all processes. Statistics are stored in Redis with appropriate TTLs.
|
|
10
|
+
class Stats
|
|
11
|
+
# Redis key prefixes
|
|
12
|
+
TOTALS_KEY = "sidekiq:patient_http:totals"
|
|
13
|
+
|
|
14
|
+
# TTLs
|
|
15
|
+
TOTALS_TTL = 30 * 24 * 60 * 60 # 30 days in seconds
|
|
16
|
+
|
|
17
|
+
def initialize(config = nil)
|
|
18
|
+
@hostname = ::Socket.gethostname.force_encoding("UTF-8").freeze
|
|
19
|
+
@pid = ::Process.pid
|
|
20
|
+
@config = config
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Record a completed request.
|
|
24
|
+
#
|
|
25
|
+
# @param status [Integer, nil] HTTP response status code
|
|
26
|
+
# @param duration [Float] request duration in seconds
|
|
27
|
+
# @return [void]
|
|
28
|
+
def record_request(status, duration)
|
|
29
|
+
::Sidekiq.redis do |redis|
|
|
30
|
+
redis.multi do |transaction|
|
|
31
|
+
transaction.hincrby(TOTALS_KEY, "requests", 1)
|
|
32
|
+
transaction.hincrbyfloat(TOTALS_KEY, "duration", duration.to_f)
|
|
33
|
+
transaction.hincrby(TOTALS_KEY, "http_status:#{status}", 1) if status && status >= 100 && status < 600
|
|
34
|
+
transaction.expire(TOTALS_KEY, TOTALS_TTL)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
rescue => e
|
|
38
|
+
handle_error(e)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Record a request error
|
|
42
|
+
#
|
|
43
|
+
# @param error_type [String] the type of error that occurred
|
|
44
|
+
# @return [void]
|
|
45
|
+
def record_error(error_type)
|
|
46
|
+
::Sidekiq.redis do |redis|
|
|
47
|
+
redis.multi do |transaction|
|
|
48
|
+
transaction.hincrby(TOTALS_KEY, "errors", 1)
|
|
49
|
+
transaction.hincrby(TOTALS_KEY, "errors:#{error_type}", 1)
|
|
50
|
+
transaction.expire(TOTALS_KEY, TOTALS_TTL)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
rescue => e
|
|
54
|
+
handle_error(e)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Record a that a request was refused because the max capacity of the Processor was reached.
|
|
58
|
+
#
|
|
59
|
+
# @return [void]
|
|
60
|
+
def record_capacity_exceeded
|
|
61
|
+
::Sidekiq.redis do |redis|
|
|
62
|
+
redis.multi do |transaction|
|
|
63
|
+
transaction.hincrby(TOTALS_KEY, "max_capacity_exceeded", 1)
|
|
64
|
+
transaction.expire(TOTALS_KEY, TOTALS_TTL)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
rescue => e
|
|
68
|
+
handle_error(e)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Get running totals
|
|
72
|
+
#
|
|
73
|
+
# @return [Hash] hash with requests, duration, errors, max_capacity_exceeded, http_status_counts
|
|
74
|
+
def get_totals
|
|
75
|
+
::Sidekiq.redis do |redis|
|
|
76
|
+
stats = redis.hgetall(TOTALS_KEY)
|
|
77
|
+
|
|
78
|
+
# Extract HTTP status counts and error type counts
|
|
79
|
+
http_status_counts = {}
|
|
80
|
+
error_type_counts = {}
|
|
81
|
+
stats.each do |key, value|
|
|
82
|
+
if key.start_with?("http_status:")
|
|
83
|
+
status = key.sub("http_status:", "").to_i
|
|
84
|
+
http_status_counts[status] = value.to_i
|
|
85
|
+
elsif key.start_with?("errors:") && key != "errors"
|
|
86
|
+
error_type = key.sub("errors:", "")
|
|
87
|
+
error_type_counts[error_type] = value.to_i
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
{
|
|
92
|
+
"requests" => (stats["requests"] || 0).to_i,
|
|
93
|
+
"duration" => (stats["duration"] || 0).to_f.round(6),
|
|
94
|
+
"errors" => (stats["errors"] || 0).to_i,
|
|
95
|
+
"max_capacity_exceeded" => (stats["max_capacity_exceeded"] || 0).to_i,
|
|
96
|
+
"http_status_counts" => http_status_counts.sort.to_h,
|
|
97
|
+
"error_type_counts" => error_type_counts.sort.to_h
|
|
98
|
+
}
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Reset all stats (useful for testing)
|
|
103
|
+
#
|
|
104
|
+
# @return [void]
|
|
105
|
+
def reset!
|
|
106
|
+
::Sidekiq.redis do |redis|
|
|
107
|
+
redis.del(TOTALS_KEY)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
private
|
|
112
|
+
|
|
113
|
+
def handle_error(error)
|
|
114
|
+
@config&.logger&.error("[PatientHttp::Sidekiq] Stats error: #{error.inspect}")
|
|
115
|
+
raise error if ::Sidekiq.testing?
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PatientHttp
|
|
4
|
+
module Sidekiq
|
|
5
|
+
# Sidekiq implementation of TaskHandler.
|
|
6
|
+
#
|
|
7
|
+
# Handles task lifecycle operations using Sidekiq for job management:
|
|
8
|
+
# - Completion and error callbacks are triggered via CallbackWorker
|
|
9
|
+
# - Large payloads are stored via ExternalStorage before enqueuing
|
|
10
|
+
# - Job retry uses Sidekiq::Client.push
|
|
11
|
+
class TaskHandler < PatientHttp::TaskHandler
|
|
12
|
+
# @return [Hash] The Sidekiq job hash containing class, jid, args, etc.
|
|
13
|
+
# Exposed for TaskMonitor crash recovery serialization.
|
|
14
|
+
attr_reader :sidekiq_job
|
|
15
|
+
|
|
16
|
+
# @param sidekiq_job [Hash] The Sidekiq job hash with "class", "jid", "args", etc.
|
|
17
|
+
def initialize(sidekiq_job)
|
|
18
|
+
@sidekiq_job = sidekiq_job
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Trigger the completion callback with the response.
|
|
22
|
+
#
|
|
23
|
+
# Stores the response via ExternalStorage (for large payloads) and
|
|
24
|
+
# enqueues a CallbackWorker to invoke the callback asynchronously.
|
|
25
|
+
#
|
|
26
|
+
# @param response [Response] the HTTP response object
|
|
27
|
+
# @param callback [String] callback class name
|
|
28
|
+
# @return [void]
|
|
29
|
+
def on_complete(response, callback)
|
|
30
|
+
data = store_if_needed(response.as_json)
|
|
31
|
+
CallbackWorker.perform_async(data, "response", callback)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Trigger the error callback with the error.
|
|
35
|
+
#
|
|
36
|
+
# Stores the error via ExternalStorage (for large payloads) and
|
|
37
|
+
# enqueues a CallbackWorker to invoke the callback asynchronously.
|
|
38
|
+
#
|
|
39
|
+
# @param error [Error] the error object
|
|
40
|
+
# @param callback [String] callback class name
|
|
41
|
+
# @return [void]
|
|
42
|
+
def on_error(error, callback)
|
|
43
|
+
data = store_if_needed(error.as_json)
|
|
44
|
+
CallbackWorker.perform_async(data, "error", callback)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Re-enqueue the original Sidekiq job for retry.
|
|
48
|
+
#
|
|
49
|
+
# @return [String] the job ID
|
|
50
|
+
def retry
|
|
51
|
+
::Sidekiq::Client.push(@sidekiq_job)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Return the job ID from the Sidekiq job.
|
|
55
|
+
#
|
|
56
|
+
# @return [String] job ID
|
|
57
|
+
def job_id
|
|
58
|
+
@sidekiq_job["jid"]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Return the worker class from the Sidekiq job.
|
|
62
|
+
#
|
|
63
|
+
# @return [Class] worker class
|
|
64
|
+
def worker_class
|
|
65
|
+
PatientHttp::ClassHelper.resolve_class_name(@sidekiq_job["class"])
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def store_if_needed(data)
|
|
71
|
+
encrypted = Sidekiq.encrypt(data)
|
|
72
|
+
external_storage = PatientHttp::Sidekiq.external_storage
|
|
73
|
+
if external_storage.enabled?
|
|
74
|
+
external_storage.store(encrypted, max_size: PatientHttp::Sidekiq.configuration.payload_store_threshold)
|
|
75
|
+
else
|
|
76
|
+
encrypted
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|