patient_http-sidekiq 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/ARCHITECTURE.md +496 -0
  3. data/CHANGELOG.md +16 -0
  4. data/MIT-LICENSE +20 -0
  5. data/README.md +620 -0
  6. data/VERSION +1 -0
  7. data/lib/patient_http/sidekiq/callback_worker.rb +96 -0
  8. data/lib/patient_http/sidekiq/configuration.rb +175 -0
  9. data/lib/patient_http/sidekiq/context.rb +61 -0
  10. data/lib/patient_http/sidekiq/lifecycle_hooks.rb +42 -0
  11. data/lib/patient_http/sidekiq/processor_observer.rb +49 -0
  12. data/lib/patient_http/sidekiq/request_executor.rb +104 -0
  13. data/lib/patient_http/sidekiq/request_worker.rb +57 -0
  14. data/lib/patient_http/sidekiq/stats.rb +119 -0
  15. data/lib/patient_http/sidekiq/task_handler.rb +81 -0
  16. data/lib/patient_http/sidekiq/task_monitor.rb +542 -0
  17. data/lib/patient_http/sidekiq/task_monitor_thread.rb +154 -0
  18. data/lib/patient_http/sidekiq/web_ui/assets/patient-http/css/patient_http.css +249 -0
  19. data/lib/patient_http/sidekiq/web_ui/locales/ar.yml +26 -0
  20. data/lib/patient_http/sidekiq/web_ui/locales/cs.yml +26 -0
  21. data/lib/patient_http/sidekiq/web_ui/locales/da.yml +26 -0
  22. data/lib/patient_http/sidekiq/web_ui/locales/de.yml +26 -0
  23. data/lib/patient_http/sidekiq/web_ui/locales/el.yml +26 -0
  24. data/lib/patient_http/sidekiq/web_ui/locales/en.yml +26 -0
  25. data/lib/patient_http/sidekiq/web_ui/locales/es.yml +26 -0
  26. data/lib/patient_http/sidekiq/web_ui/locales/fa.yml +26 -0
  27. data/lib/patient_http/sidekiq/web_ui/locales/fr.yml +26 -0
  28. data/lib/patient_http/sidekiq/web_ui/locales/gd.yml +26 -0
  29. data/lib/patient_http/sidekiq/web_ui/locales/he.yml +26 -0
  30. data/lib/patient_http/sidekiq/web_ui/locales/hi.yml +26 -0
  31. data/lib/patient_http/sidekiq/web_ui/locales/it.yml +26 -0
  32. data/lib/patient_http/sidekiq/web_ui/locales/ja.yml +26 -0
  33. data/lib/patient_http/sidekiq/web_ui/locales/ko.yml +26 -0
  34. data/lib/patient_http/sidekiq/web_ui/locales/lt.yml +26 -0
  35. data/lib/patient_http/sidekiq/web_ui/locales/nb.yml +26 -0
  36. data/lib/patient_http/sidekiq/web_ui/locales/nl.yml +26 -0
  37. data/lib/patient_http/sidekiq/web_ui/locales/pl.yml +26 -0
  38. data/lib/patient_http/sidekiq/web_ui/locales/pt-BR.yml +26 -0
  39. data/lib/patient_http/sidekiq/web_ui/locales/pt.yml +26 -0
  40. data/lib/patient_http/sidekiq/web_ui/locales/ru.yml +26 -0
  41. data/lib/patient_http/sidekiq/web_ui/locales/sv.yml +26 -0
  42. data/lib/patient_http/sidekiq/web_ui/locales/ta.yml +26 -0
  43. data/lib/patient_http/sidekiq/web_ui/locales/tr.yml +26 -0
  44. data/lib/patient_http/sidekiq/web_ui/locales/uk.yml +26 -0
  45. data/lib/patient_http/sidekiq/web_ui/locales/ur.yml +26 -0
  46. data/lib/patient_http/sidekiq/web_ui/locales/vi.yml +26 -0
  47. data/lib/patient_http/sidekiq/web_ui/locales/zh-CN.yml +26 -0
  48. data/lib/patient_http/sidekiq/web_ui/locales/zh-TW.yml +26 -0
  49. data/lib/patient_http/sidekiq/web_ui/views/patient_http.html.erb +142 -0
  50. data/lib/patient_http/sidekiq/web_ui.rb +69 -0
  51. data/lib/patient_http/sidekiq.rb +328 -0
  52. data/lib/patient_http-sidekiq.rb +3 -0
  53. data/patient_http-sidekiq.gemspec +46 -0
  54. metadata +140 -0
@@ -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