solid_queue_guard 0.1.0 → 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 +4 -4
- data/.gitignore +12 -0
- data/.rubocop.yml +44 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +81 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +308 -0
- data/README.md +79 -14
- data/app/controllers/solid_queue_guard/application_controller.rb +1 -0
- data/app/controllers/solid_queue_guard/health_controller.rb +22 -1
- data/lib/generators/solid_queue_guard/install/USAGE.ci +9 -0
- data/lib/generators/solid_queue_guard/install/ci_generator.rb +19 -0
- data/lib/generators/solid_queue_guard/install/templates/solid_queue_guard.rb +11 -1
- data/lib/generators/solid_queue_guard/install/templates/solid_queue_guard.yml +25 -0
- data/lib/solid_queue_guard/checks/base.rb +16 -0
- data/lib/solid_queue_guard/checks/config/async_supervisor_config_check.rb +25 -0
- data/lib/solid_queue_guard/checks/config/puma_colocated_check.rb +5 -11
- data/lib/solid_queue_guard/checks/config/topology_recommendation_check.rb +24 -0
- data/lib/solid_queue_guard/checks/registry.rb +9 -2
- data/lib/solid_queue_guard/checks/runtime/base.rb +1 -5
- data/lib/solid_queue_guard/checks/runtime/blocked_jobs_check.rb +20 -1
- data/lib/solid_queue_guard/checks/runtime/database_support.rb +40 -0
- data/lib/solid_queue_guard/checks/runtime/dispatcher_check.rb +26 -1
- data/lib/solid_queue_guard/checks/runtime/failed_jobs_check.rb +21 -1
- data/lib/solid_queue_guard/checks/runtime/finished_jobs_growth_check.rb +20 -1
- data/lib/solid_queue_guard/checks/runtime/orphaned_claims_check.rb +18 -1
- data/lib/solid_queue_guard/checks/runtime/paused_queue_lag_check.rb +22 -1
- data/lib/solid_queue_guard/checks/runtime/pidfile_check.rb +33 -1
- data/lib/solid_queue_guard/checks/runtime/process_topology_check.rb +31 -1
- data/lib/solid_queue_guard/checks/runtime/puma_plugin_runtime_check.rb +39 -0
- data/lib/solid_queue_guard/checks/runtime/queue_lag_check.rb +30 -1
- data/lib/solid_queue_guard/checks/runtime/recurring_stale_check.rb +29 -1
- data/lib/solid_queue_guard/checks/runtime/scheduled_backlog_check.rb +18 -1
- data/lib/solid_queue_guard/checks/runtime/stale_process_check.rb +19 -1
- data/lib/solid_queue_guard/cli.rb +10 -1
- data/lib/solid_queue_guard/configuration.rb +36 -1
- data/lib/solid_queue_guard/engine.rb +12 -0
- data/lib/solid_queue_guard/formatters/terminal.rb +1 -1
- data/lib/solid_queue_guard/health/cache.rb +28 -0
- data/lib/solid_queue_guard/health/payload.rb +50 -0
- data/lib/solid_queue_guard/http_status_policy.rb +35 -0
- data/lib/solid_queue_guard/metrics/exporter.rb +34 -0
- data/lib/solid_queue_guard/metrics/open_telemetry.rb +19 -0
- data/lib/solid_queue_guard/metrics/prometheus.rb +21 -0
- data/lib/solid_queue_guard/metrics/statsd.rb +25 -0
- data/lib/solid_queue_guard/notifier.rb +76 -0
- data/lib/solid_queue_guard/notifiers/base.rb +2 -7
- data/lib/solid_queue_guard/puma_plugin_support.rb +35 -0
- data/lib/solid_queue_guard/recommendations/topology.rb +76 -0
- data/lib/solid_queue_guard/runner.rb +10 -1
- data/lib/solid_queue_guard/tasks.rb +1 -1
- data/lib/solid_queue_guard/version.rb +1 -1
- data/lib/solid_queue_guard.rb +1 -1
- data/solid_queue_guard.gemspec +54 -0
- metadata +25 -1
|
@@ -3,7 +3,25 @@
|
|
|
3
3
|
module SolidQueueGuard
|
|
4
4
|
module Checks
|
|
5
5
|
module Runtime
|
|
6
|
-
class StaleProcessCheck < Base
|
|
6
|
+
class StaleProcessCheck < Base
|
|
7
|
+
def call
|
|
8
|
+
with_queue_database do
|
|
9
|
+
threshold = config.check_setting(:stale_process, :threshold, config.stale_process_threshold)
|
|
10
|
+
stale = SolidQueue::Process.where(last_heartbeat_at: ...threshold.ago)
|
|
11
|
+
|
|
12
|
+
if stale.none?
|
|
13
|
+
pass(check_id, 'All Solid Queue processes have recent heartbeats')
|
|
14
|
+
else
|
|
15
|
+
failure(
|
|
16
|
+
check_id,
|
|
17
|
+
"#{stale.count} Solid Queue process(es) have stale heartbeats",
|
|
18
|
+
suggestion: 'Restart the Solid Queue supervisor and verify workers are running',
|
|
19
|
+
metadata: { stale_processes: stale.count }
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
7
25
|
end
|
|
8
26
|
end
|
|
9
27
|
end
|
|
@@ -18,9 +18,12 @@ module SolidQueueGuard
|
|
|
18
18
|
strict = strict_flag?(argv) if strict.nil?
|
|
19
19
|
|
|
20
20
|
report = Runner.new(scope: scope).run
|
|
21
|
+
Notifier.deliver_all(report) if notify?(report)
|
|
22
|
+
Metrics::Exporter.export(report) if SolidQueueGuard.config.metrics_backends.any?
|
|
23
|
+
|
|
21
24
|
output = formatter_for(format).new(report).render
|
|
22
25
|
|
|
23
|
-
|
|
26
|
+
$stdout.puts(output) unless output.empty?
|
|
24
27
|
|
|
25
28
|
exit report.exit_code(strict: strict)
|
|
26
29
|
end
|
|
@@ -51,5 +54,11 @@ module SolidQueueGuard
|
|
|
51
54
|
ENV.fetch('SOLID_QUEUE_GUARD_SCOPE', 'config').to_sym
|
|
52
55
|
end
|
|
53
56
|
end
|
|
57
|
+
|
|
58
|
+
def notify?(report)
|
|
59
|
+
return false unless SolidQueueGuard.config.notify_with.any?
|
|
60
|
+
|
|
61
|
+
report.status != :healthy
|
|
62
|
+
end
|
|
54
63
|
end
|
|
55
64
|
end
|
|
@@ -11,7 +11,12 @@ module SolidQueueGuard
|
|
|
11
11
|
:health_cache_ttl,
|
|
12
12
|
:scheduled_backlog_threshold,
|
|
13
13
|
:integrate_rails_health,
|
|
14
|
-
:notify_with
|
|
14
|
+
:notify_with,
|
|
15
|
+
:metrics_backends,
|
|
16
|
+
:disabled_checks,
|
|
17
|
+
:checks,
|
|
18
|
+
:degraded_http_status,
|
|
19
|
+
:unhealthy_http_status
|
|
15
20
|
|
|
16
21
|
def initialize
|
|
17
22
|
@enabled = true
|
|
@@ -24,10 +29,40 @@ module SolidQueueGuard
|
|
|
24
29
|
@scheduled_backlog_threshold = 100
|
|
25
30
|
@integrate_rails_health = false
|
|
26
31
|
@notify_with = [:rails_logger]
|
|
32
|
+
@metrics_backends = []
|
|
33
|
+
@disabled_checks = []
|
|
34
|
+
@checks = ActiveSupport::OrderedOptions.new
|
|
35
|
+
@degraded_http_status = :ok
|
|
36
|
+
@unhealthy_http_status = :service_unavailable
|
|
27
37
|
end
|
|
28
38
|
|
|
29
39
|
def strict?
|
|
30
40
|
strict_mode || ActiveModel::Type::Boolean.new.cast(ENV.fetch('SOLID_QUEUE_GUARD_STRICT', nil))
|
|
31
41
|
end
|
|
42
|
+
|
|
43
|
+
def check_enabled?(check_id)
|
|
44
|
+
id = check_id.to_sym
|
|
45
|
+
return false if disabled_checks.map(&:to_sym).include?(id)
|
|
46
|
+
|
|
47
|
+
settings = check_settings_for(id)
|
|
48
|
+
return true if settings.nil?
|
|
49
|
+
|
|
50
|
+
enabled_value = settings[:enabled]
|
|
51
|
+
enabled_value = settings['enabled'] if enabled_value.nil?
|
|
52
|
+
enabled_value != false
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def check_settings_for(check_id)
|
|
56
|
+
checks[check_id.to_sym] || checks[check_id.to_s]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def check_setting(check_id, key, default = nil)
|
|
60
|
+
settings = check_settings_for(check_id)
|
|
61
|
+
return default unless settings
|
|
62
|
+
|
|
63
|
+
value = settings[key]
|
|
64
|
+
value = settings[key.to_sym] if value.nil?
|
|
65
|
+
value.nil? ? default : value
|
|
66
|
+
end
|
|
32
67
|
end
|
|
33
68
|
end
|
|
@@ -17,5 +17,17 @@ module SolidQueueGuard
|
|
|
17
17
|
end
|
|
18
18
|
end
|
|
19
19
|
end
|
|
20
|
+
|
|
21
|
+
initializer 'solid_queue_guard.rails_health' do
|
|
22
|
+
next unless SolidQueueGuard.config.integrate_rails_health
|
|
23
|
+
|
|
24
|
+
ActiveSupport.on_load(:action_controller_base) do
|
|
25
|
+
Rails::HealthController.class_eval do
|
|
26
|
+
def solid_queue_guard_status
|
|
27
|
+
SolidQueueGuard::Health::Cache.fetch[:status]
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
20
32
|
end
|
|
21
33
|
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueueGuard
|
|
4
|
+
module Health
|
|
5
|
+
class Cache
|
|
6
|
+
def self.fetch
|
|
7
|
+
new.fetch
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def fetch
|
|
11
|
+
Rails.cache.fetch(cache_key, expires_in: SolidQueueGuard.config.health_cache_ttl) do
|
|
12
|
+
build_payload
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def cache_key
|
|
19
|
+
'solid_queue_guard/health'
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def build_payload
|
|
23
|
+
report = Runner.new(scope: :all).run
|
|
24
|
+
Payload.new(report).to_h
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueueGuard
|
|
4
|
+
module Health
|
|
5
|
+
class Payload
|
|
6
|
+
def initialize(report)
|
|
7
|
+
@report = report
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def to_h
|
|
11
|
+
base = report.to_h.merge(
|
|
12
|
+
queue_lag_seconds: queue_lag_seconds,
|
|
13
|
+
failed_jobs_last_hour: failed_jobs_last_hour,
|
|
14
|
+
dead_processes: dead_processes
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
base[:recommendations] = topology_recommendations if topology_recommendations.any?
|
|
18
|
+
base
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
attr_reader :report
|
|
24
|
+
|
|
25
|
+
def queue_lag_seconds
|
|
26
|
+
metadata_value('queue_lag') { |metadata| metadata[:queue_lag_seconds] }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def failed_jobs_last_hour
|
|
30
|
+
metadata_value('failed_jobs') { |metadata| metadata[:failed_jobs_last_hour] } || 0
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def dead_processes
|
|
34
|
+
metadata_value('stale_process') { |metadata| metadata[:stale_processes] } || 0
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def metadata_value(check_id)
|
|
38
|
+
result = report.results.find { |entry| entry.id == check_id }
|
|
39
|
+
metadata = result&.metadata || {}
|
|
40
|
+
value = yield(metadata)
|
|
41
|
+
value unless value.nil?
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def topology_recommendations
|
|
45
|
+
result = report.results.find { |entry| entry.id == 'topology_recommendation' }
|
|
46
|
+
result&.metadata&.fetch(:recommendations, []) || []
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueueGuard
|
|
4
|
+
# @api private
|
|
5
|
+
module HttpStatusPolicy
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
STATUS_MAP = {
|
|
9
|
+
ok: :ok,
|
|
10
|
+
success: :ok,
|
|
11
|
+
200 => :ok,
|
|
12
|
+
multi_status: :multi_status,
|
|
13
|
+
207 => :multi_status,
|
|
14
|
+
service_unavailable: :service_unavailable,
|
|
15
|
+
unavailable: :service_unavailable,
|
|
16
|
+
503 => :service_unavailable
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
def resolve(status)
|
|
20
|
+
key = status.is_a?(Integer) ? status : status.to_sym
|
|
21
|
+
STATUS_MAP.fetch(key, :ok)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def for_report_status(report_status, config: SolidQueueGuard.config)
|
|
25
|
+
case report_status.to_s
|
|
26
|
+
when 'unhealthy'
|
|
27
|
+
resolve(config.unhealthy_http_status)
|
|
28
|
+
when 'degraded'
|
|
29
|
+
resolve(config.degraded_http_status)
|
|
30
|
+
else
|
|
31
|
+
:ok
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueueGuard
|
|
4
|
+
module Metrics
|
|
5
|
+
class Exporter
|
|
6
|
+
STATUS_VALUES = { healthy: 0, degraded: 1, unhealthy: 2 }.freeze
|
|
7
|
+
|
|
8
|
+
def self.export(report, backends: SolidQueueGuard.config.metrics_backends)
|
|
9
|
+
new(report, backends: backends).export
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize(report, backends:)
|
|
13
|
+
@report = report
|
|
14
|
+
@backends = Array(backends)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def export
|
|
18
|
+
backends.each { |backend| export_backend(backend) }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
attr_reader :report, :backends
|
|
24
|
+
|
|
25
|
+
def export_backend(backend)
|
|
26
|
+
case backend.to_sym
|
|
27
|
+
when :statsd then Statsd.export(report)
|
|
28
|
+
when :prometheus then Prometheus.export(report)
|
|
29
|
+
when :opentelemetry then OpenTelemetry.export(report)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueueGuard
|
|
4
|
+
module Metrics
|
|
5
|
+
module OpenTelemetry
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def export(report)
|
|
9
|
+
return unless defined?(::OpenTelemetry)
|
|
10
|
+
|
|
11
|
+
meter = ::OpenTelemetry.meter_provider.meter('solid_queue_guard')
|
|
12
|
+
gauge = meter.create_gauge('solid_queue.guard.overall_status', unit: 'status')
|
|
13
|
+
gauge.record(Exporter::STATUS_VALUES.fetch(report.status))
|
|
14
|
+
rescue StandardError
|
|
15
|
+
nil
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueueGuard
|
|
4
|
+
module Metrics
|
|
5
|
+
module Prometheus
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def export(report)
|
|
9
|
+
path = ENV.fetch('SOLID_QUEUE_GUARD_PROMETHEUS_FILE', Rails.root.join('tmp/solid_queue_guard.prom'))
|
|
10
|
+
File.write(path, render(report))
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def render(report)
|
|
14
|
+
<<~PROM
|
|
15
|
+
# TYPE solid_queue_guard_overall_status gauge
|
|
16
|
+
solid_queue_guard_overall_status #{Exporter::STATUS_VALUES.fetch(report.status)}
|
|
17
|
+
PROM
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueueGuard
|
|
4
|
+
module Metrics
|
|
5
|
+
module Statsd
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def export(report)
|
|
9
|
+
require 'socket'
|
|
10
|
+
host = ENV.fetch('SOLID_QUEUE_GUARD_STATSD_HOST', '127.0.0.1')
|
|
11
|
+
port = ENV.fetch('SOLID_QUEUE_GUARD_STATSD_PORT', 8125).to_i
|
|
12
|
+
socket = UDPSocket.new
|
|
13
|
+
socket.send(metric_line(report), 0, host, port)
|
|
14
|
+
ensure
|
|
15
|
+
socket&.close
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def metric_line(report)
|
|
19
|
+
"solid_queue.guard.overall_status:#{STATUS_VALUES.fetch(report.status)}|g"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
STATUS_VALUES = Exporter::STATUS_VALUES
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueueGuard
|
|
4
|
+
class Notifier
|
|
5
|
+
class << self
|
|
6
|
+
def deliver_all(report, adapters: SolidQueueGuard.config.notify_with)
|
|
7
|
+
Array(adapters).each do |adapter|
|
|
8
|
+
deliver(adapter, report)
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def deliver(adapter, report)
|
|
13
|
+
case adapter.to_sym
|
|
14
|
+
when :slack then deliver_slack(report)
|
|
15
|
+
when :datadog then deliver_datadog(report)
|
|
16
|
+
when :webhook then deliver_webhook(report)
|
|
17
|
+
else deliver_rails_logger(report)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def post_json(url, payload, headers: {})
|
|
22
|
+
require 'net/http'
|
|
23
|
+
uri = URI(url)
|
|
24
|
+
request = Net::HTTP::Post.new(uri)
|
|
25
|
+
request['Content-Type'] = 'application/json'
|
|
26
|
+
headers.each { |key, value| request[key] = value }
|
|
27
|
+
request.body = JSON.generate(payload)
|
|
28
|
+
Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
|
|
29
|
+
http.request(request)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def deliver_rails_logger(report)
|
|
36
|
+
Rails.logger.warn("[SolidQueueGuard] status=#{report.status} warnings=#{report.warnings.join('; ')}")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def deliver_slack(report)
|
|
40
|
+
webhook_url = ENV.fetch('SOLID_QUEUE_GUARD_SLACK_WEBHOOK_URL', nil)
|
|
41
|
+
return if webhook_url.blank?
|
|
42
|
+
|
|
43
|
+
post_json(webhook_url, { text: "SolidQueueGuard status: *#{report.status}*\n#{report.warnings.join("\n")}" })
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def deliver_datadog(report)
|
|
47
|
+
api_key = ENV.fetch('DD_API_KEY', nil)
|
|
48
|
+
return if api_key.blank?
|
|
49
|
+
|
|
50
|
+
alert_type = case report.status
|
|
51
|
+
when :unhealthy then 'error'
|
|
52
|
+
when :degraded then 'warning'
|
|
53
|
+
else 'info'
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
post_json(
|
|
57
|
+
'https://api.datadoghq.com/api/v1/events',
|
|
58
|
+
{
|
|
59
|
+
title: 'SolidQueueGuard alert',
|
|
60
|
+
text: report.warnings.join("\n"),
|
|
61
|
+
alert_type: alert_type,
|
|
62
|
+
tags: ["status:#{report.status}"]
|
|
63
|
+
},
|
|
64
|
+
headers: { 'DD-API-KEY' => api_key }
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def deliver_webhook(report)
|
|
69
|
+
webhook_url = ENV.fetch('SOLID_QUEUE_GUARD_WEBHOOK_URL', nil)
|
|
70
|
+
return if webhook_url.blank?
|
|
71
|
+
|
|
72
|
+
post_json(webhook_url, report.to_h)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -4,14 +4,9 @@ module SolidQueueGuard
|
|
|
4
4
|
module Notifiers
|
|
5
5
|
# @api private
|
|
6
6
|
class Base
|
|
7
|
-
def self.deliver(
|
|
8
|
-
|
|
7
|
+
def self.deliver(report)
|
|
8
|
+
Notifier.deliver(:rails_logger, report)
|
|
9
9
|
end
|
|
10
10
|
end
|
|
11
|
-
|
|
12
|
-
class RailsLogger < Base; end
|
|
13
|
-
class Slack < Base; end
|
|
14
|
-
class Datadog < Base; end
|
|
15
|
-
class Webhook < Base; end
|
|
16
11
|
end
|
|
17
12
|
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueueGuard
|
|
4
|
+
# @api private
|
|
5
|
+
module PumaPluginSupport
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
PUMA_PLUGIN_PATTERN = /plugin\s+:?solid_queue/
|
|
9
|
+
ASYNC_MODE_PATTERN = /solid_queue_mode\s+:?async/
|
|
10
|
+
|
|
11
|
+
def puma_config_path
|
|
12
|
+
Rails.root.join('config/puma.rb')
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def puma_config_content
|
|
16
|
+
return unless puma_config_path.exist?
|
|
17
|
+
|
|
18
|
+
puma_config_path.read
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def puma_plugin_enabled?
|
|
22
|
+
content = puma_config_content
|
|
23
|
+
content.present? && content.match?(PUMA_PLUGIN_PATTERN)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def puma_async_mode?
|
|
27
|
+
content = puma_config_content
|
|
28
|
+
content.present? && content.match?(ASYNC_MODE_PATTERN)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def async_supervisor_mode?
|
|
32
|
+
ENV.fetch('SOLID_QUEUE_SUPERVISOR_MODE', 'fork').casecmp('async').zero? || puma_async_mode?
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueueGuard
|
|
4
|
+
module Recommendations
|
|
5
|
+
class Topology
|
|
6
|
+
def self.analyze(configuration: SolidQueue::Configuration.new)
|
|
7
|
+
new(configuration: configuration).analyze
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def initialize(configuration:)
|
|
11
|
+
@configuration = configuration
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def analyze
|
|
15
|
+
recommendations = []
|
|
16
|
+
recommendations.concat(worker_recommendations)
|
|
17
|
+
recommendations.concat(pool_recommendations)
|
|
18
|
+
recommendations.uniq
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
attr_reader :configuration
|
|
24
|
+
|
|
25
|
+
def worker_recommendations
|
|
26
|
+
worker_queues = QueueCoverage.worker_queues_from_configuration(configuration)
|
|
27
|
+
uncovered = uncovered_queues(worker_queues)
|
|
28
|
+
return [] if uncovered.empty?
|
|
29
|
+
|
|
30
|
+
["Add worker coverage for: #{uncovered.join(', ')}"]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def pool_recommendations
|
|
34
|
+
return async_pool_recommendation if PumaPluginSupport.async_supervisor_mode?
|
|
35
|
+
|
|
36
|
+
required_threads = configuration.estimated_number_of_threads
|
|
37
|
+
pool_size = SolidQueue::Record.connection_pool&.size
|
|
38
|
+
return [] if pool_size.nil? || required_threads <= pool_size
|
|
39
|
+
|
|
40
|
+
["Increase queue DB pool to at least #{required_threads + 2} (currently #{pool_size})"]
|
|
41
|
+
rescue StandardError
|
|
42
|
+
[]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def uncovered_queues(assigned_queues)
|
|
46
|
+
observed = observed_queues
|
|
47
|
+
(observed - assigned_queues).sort
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def observed_queues
|
|
51
|
+
return [] unless database_available?
|
|
52
|
+
|
|
53
|
+
SolidQueue::Job.distinct.pluck(:queue_name)
|
|
54
|
+
rescue StandardError
|
|
55
|
+
[]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def database_available?
|
|
59
|
+
SolidQueue::Record.connection_pool&.with_connection(&:active?)
|
|
60
|
+
rescue StandardError
|
|
61
|
+
false
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def async_pool_recommendation
|
|
65
|
+
required_threads = configuration.estimated_number_of_threads
|
|
66
|
+
pool_size = SolidQueue::Record.connection_pool&.size
|
|
67
|
+
return [] if pool_size.nil? || required_threads <= pool_size
|
|
68
|
+
|
|
69
|
+
["Increase queue DB pool to at least #{required_threads + 2} " \
|
|
70
|
+
"for async supervisor mode (currently #{pool_size})"]
|
|
71
|
+
rescue StandardError
|
|
72
|
+
[]
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -27,10 +27,19 @@ module SolidQueueGuard
|
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
def run_check(check_class)
|
|
30
|
+
check_id = Checks::Registry.check_id_for(check_class)
|
|
31
|
+
unless SolidQueueGuard.config.check_enabled?(check_id)
|
|
32
|
+
return Check::Result.new(
|
|
33
|
+
id: check_id,
|
|
34
|
+
status: :skip,
|
|
35
|
+
message: 'Check disabled via configuration'
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
|
|
30
39
|
check_class.call
|
|
31
40
|
rescue StandardError => e
|
|
32
41
|
Check::Result.new(
|
|
33
|
-
id:
|
|
42
|
+
id: check_id,
|
|
34
43
|
status: :fail,
|
|
35
44
|
message: "Check raised an error: #{e.class}: #{e.message}"
|
|
36
45
|
)
|
|
@@ -17,7 +17,7 @@ namespace :solid_queue_guard do
|
|
|
17
17
|
|
|
18
18
|
desc 'Print machine-readable health status'
|
|
19
19
|
task health: :environment do
|
|
20
|
-
SolidQueueGuard::CLI.run!(scope: :
|
|
20
|
+
SolidQueueGuard::CLI.run!(scope: :all, format: :json, strict: cli_strict)
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
desc 'Print full diagnostic report. Usage: report[json]'
|
data/lib/solid_queue_guard.rb
CHANGED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'lib/solid_queue_guard/version'
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = 'solid_queue_guard'
|
|
7
|
+
spec.version = SolidQueueGuard::VERSION
|
|
8
|
+
spec.authors = ['Rafael Pissardo']
|
|
9
|
+
spec.email = ['rpissardo@users.noreply.github.com']
|
|
10
|
+
spec.homepage = 'https://github.com/rafael-pissardo/solid_queue_guard'
|
|
11
|
+
spec.summary = 'Production readiness checks and runtime guards for Rails Solid Queue.'
|
|
12
|
+
spec.description = [
|
|
13
|
+
'Detect queue lag, dead workers, unsafe thread/pool configuration,',
|
|
14
|
+
'and broken recurring jobs before they become incidents.'
|
|
15
|
+
].join(' ')
|
|
16
|
+
spec.license = 'MIT'
|
|
17
|
+
|
|
18
|
+
spec.metadata = {
|
|
19
|
+
'homepage_uri' => spec.homepage,
|
|
20
|
+
'source_code_uri' => 'https://github.com/rafael-pissardo/solid_queue_guard',
|
|
21
|
+
'changelog_uri' => 'https://github.com/rafael-pissardo/solid_queue_guard/blob/main/CHANGELOG.md',
|
|
22
|
+
'bug_tracker_uri' => 'https://github.com/rafael-pissardo/solid_queue_guard/issues',
|
|
23
|
+
'documentation_uri' => 'https://github.com/rafael-pissardo/solid_queue_guard#readme',
|
|
24
|
+
'rubygems_mfa_required' => 'true'
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
28
|
+
`git ls-files -z`.split("\x0").reject do |file|
|
|
29
|
+
file.start_with?('test/', '.github/', 'docs/', 'script/', 'gemfiles/', 'Appraisals')
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
spec.require_paths = ['lib']
|
|
34
|
+
spec.required_ruby_version = '>= 3.1'
|
|
35
|
+
|
|
36
|
+
rails_version = '>= 7.1', '< 9.0'
|
|
37
|
+
|
|
38
|
+
spec.add_dependency 'actionpack', rails_version
|
|
39
|
+
spec.add_dependency 'activejob', rails_version
|
|
40
|
+
spec.add_dependency 'activerecord', rails_version
|
|
41
|
+
spec.add_dependency 'activesupport', rails_version
|
|
42
|
+
spec.add_dependency 'railties', rails_version
|
|
43
|
+
spec.add_dependency 'solid_queue', '>= 1.0', '< 2.0'
|
|
44
|
+
|
|
45
|
+
spec.add_development_dependency 'appraisal', '~> 2.5'
|
|
46
|
+
spec.add_development_dependency 'debug'
|
|
47
|
+
spec.add_development_dependency 'minitest', '~> 5.0'
|
|
48
|
+
spec.add_development_dependency 'mocha', '>= 2.1'
|
|
49
|
+
spec.add_development_dependency 'puma', '>= 6.0'
|
|
50
|
+
spec.add_development_dependency 'rails', rails_version
|
|
51
|
+
spec.add_development_dependency 'rubocop', '~> 1.75'
|
|
52
|
+
spec.add_development_dependency 'rubocop-rails', '~> 2.30'
|
|
53
|
+
spec.add_development_dependency 'sqlite3', '~> 2.1'
|
|
54
|
+
end
|