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
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails/generators/base'
|
|
4
|
+
|
|
5
|
+
module SolidQueueGuard
|
|
6
|
+
module Generators
|
|
7
|
+
module Install
|
|
8
|
+
class CiGenerator < Rails::Generators::Base
|
|
9
|
+
source_root File.expand_path('templates', __dir__)
|
|
10
|
+
|
|
11
|
+
desc 'Adds a GitHub Actions workflow that runs solid_queue_guard:doctor in CI'
|
|
12
|
+
|
|
13
|
+
def copy_workflow
|
|
14
|
+
template 'solid_queue_guard.yml', '.github/workflows/solid_queue_guard.yml'
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -11,7 +11,17 @@ SolidQueueGuard.configure do |config|
|
|
|
11
11
|
|
|
12
12
|
config.failed_jobs_threshold = 20
|
|
13
13
|
config.stale_process_threshold = 5.minutes
|
|
14
|
+
config.health_cache_ttl = 15.seconds
|
|
15
|
+
config.scheduled_backlog_threshold = 100
|
|
16
|
+
|
|
17
|
+
# config.disabled_checks = [:pidfile]
|
|
18
|
+
# config.checks.queue_lag = { threshold: 10.minutes }
|
|
19
|
+
# config.checks.failed_jobs = { threshold: 5, enabled: true }
|
|
20
|
+
# config.degraded_http_status = 207
|
|
21
|
+
# config.unhealthy_http_status = 503
|
|
14
22
|
|
|
15
23
|
# config.health_token = ENV["SOLID_QUEUE_GUARD_TOKEN"]
|
|
16
|
-
# config.
|
|
24
|
+
# config.integrate_rails_health = true
|
|
25
|
+
# config.notify_with = [:rails_logger, :slack, :datadog, :webhook]
|
|
26
|
+
# config.metrics_backends = [:statsd, :prometheus, :opentelemetry]
|
|
17
27
|
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
name: Solid Queue Guard
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
doctor:
|
|
10
|
+
name: Solid Queue production readiness
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
|
|
16
|
+
- uses: ruby/setup-ruby@v1
|
|
17
|
+
with:
|
|
18
|
+
ruby-version: .ruby-version
|
|
19
|
+
bundler-cache: true
|
|
20
|
+
|
|
21
|
+
- name: Prepare test database
|
|
22
|
+
run: bin/rails db:test:prepare
|
|
23
|
+
|
|
24
|
+
- name: Run Solid Queue Guard doctor
|
|
25
|
+
run: SOLID_QUEUE_GUARD_STRICT=1 bin/rails solid_queue_guard:doctor
|
|
@@ -8,6 +8,10 @@ module SolidQueueGuard
|
|
|
8
8
|
new(**options).call
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
+
def self.check_id
|
|
12
|
+
name.demodulize.underscore.delete_suffix('_check')
|
|
13
|
+
end
|
|
14
|
+
|
|
11
15
|
def initialize(**options)
|
|
12
16
|
@options = options
|
|
13
17
|
end
|
|
@@ -47,6 +51,18 @@ module SolidQueueGuard
|
|
|
47
51
|
def solid_queue_configuration
|
|
48
52
|
@solid_queue_configuration ||= SolidQueue::Configuration.new
|
|
49
53
|
end
|
|
54
|
+
|
|
55
|
+
def check_id
|
|
56
|
+
self.class.check_id
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def guard_config
|
|
60
|
+
SolidQueueGuard.config
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def check_setting(key, default = nil)
|
|
64
|
+
guard_config.check_setting(check_id, key, default)
|
|
65
|
+
end
|
|
50
66
|
end
|
|
51
67
|
end
|
|
52
68
|
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueueGuard
|
|
4
|
+
module Checks
|
|
5
|
+
module Config
|
|
6
|
+
class AsyncSupervisorConfigCheck < Base
|
|
7
|
+
def call
|
|
8
|
+
if PumaPluginSupport.async_supervisor_mode?
|
|
9
|
+
warn(
|
|
10
|
+
check_id,
|
|
11
|
+
'Solid Queue supervisor is running in async mode',
|
|
12
|
+
suggestion: [
|
|
13
|
+
'Review thread and database pool sizing;',
|
|
14
|
+
'the processes option in queue.yml is ignored in async mode'
|
|
15
|
+
].join(' '),
|
|
16
|
+
metadata: { supervisor_mode: 'async' }
|
|
17
|
+
)
|
|
18
|
+
else
|
|
19
|
+
pass(check_id, 'Solid Queue supervisor is running in fork mode')
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -4,27 +4,21 @@ module SolidQueueGuard
|
|
|
4
4
|
module Checks
|
|
5
5
|
module Config
|
|
6
6
|
class PumaColocatedCheck < Base
|
|
7
|
-
PUMA_PLUGIN_PATTERN = /plugin\s+:?solid_queue/
|
|
8
|
-
|
|
9
7
|
def call
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
return pass('puma_colocated', 'No config/puma.rb found') unless puma_config.exist?
|
|
13
|
-
|
|
14
|
-
content = puma_config.read
|
|
8
|
+
return pass(check_id, 'No config/puma.rb found') unless PumaPluginSupport.puma_config_path.exist?
|
|
15
9
|
|
|
16
|
-
if
|
|
10
|
+
if PumaPluginSupport.puma_plugin_enabled?
|
|
17
11
|
if Rails.env.production?
|
|
18
12
|
warn(
|
|
19
|
-
|
|
13
|
+
check_id,
|
|
20
14
|
'Solid Queue Puma plugin is enabled in production',
|
|
21
15
|
suggestion: 'Run Solid Queue in a dedicated job process for better isolation and memory management'
|
|
22
16
|
)
|
|
23
17
|
else
|
|
24
|
-
pass(
|
|
18
|
+
pass(check_id, 'Solid Queue Puma plugin detected (non-production environment)')
|
|
25
19
|
end
|
|
26
20
|
else
|
|
27
|
-
pass(
|
|
21
|
+
pass(check_id, 'Solid Queue is not co-located with Puma')
|
|
28
22
|
end
|
|
29
23
|
end
|
|
30
24
|
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueueGuard
|
|
4
|
+
module Checks
|
|
5
|
+
module Config
|
|
6
|
+
class TopologyRecommendationCheck < Base
|
|
7
|
+
def call
|
|
8
|
+
recommendations = Recommendations::Topology.analyze
|
|
9
|
+
|
|
10
|
+
if recommendations.empty?
|
|
11
|
+
pass('topology_recommendation', 'No queue.yml topology changes recommended')
|
|
12
|
+
else
|
|
13
|
+
warn(
|
|
14
|
+
'topology_recommendation',
|
|
15
|
+
recommendations.join('; '),
|
|
16
|
+
suggestion: 'Review config/queue.yml worker and database pool settings',
|
|
17
|
+
metadata: { recommendations: recommendations }
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -14,7 +14,9 @@ module SolidQueueGuard
|
|
|
14
14
|
Config::SchedulerConfigCheck,
|
|
15
15
|
Config::EnvFlagsCheck,
|
|
16
16
|
Config::ProcessHeartbeatConfigCheck,
|
|
17
|
-
Config::PumaColocatedCheck
|
|
17
|
+
Config::PumaColocatedCheck,
|
|
18
|
+
Config::TopologyRecommendationCheck,
|
|
19
|
+
Config::AsyncSupervisorConfigCheck
|
|
18
20
|
].freeze
|
|
19
21
|
|
|
20
22
|
RUNTIME_CHECKS = [
|
|
@@ -29,10 +31,15 @@ module SolidQueueGuard
|
|
|
29
31
|
Runtime::RecurringStaleCheck,
|
|
30
32
|
Runtime::PausedQueueLagCheck,
|
|
31
33
|
Runtime::PidfileCheck,
|
|
32
|
-
Runtime::FinishedJobsGrowthCheck
|
|
34
|
+
Runtime::FinishedJobsGrowthCheck,
|
|
35
|
+
Runtime::PumaPluginRuntimeCheck
|
|
33
36
|
].freeze
|
|
34
37
|
|
|
35
38
|
class << self
|
|
39
|
+
def check_id_for(check_class)
|
|
40
|
+
check_class.check_id
|
|
41
|
+
end
|
|
42
|
+
|
|
36
43
|
def for(scope)
|
|
37
44
|
case scope.to_sym
|
|
38
45
|
when :config then CONFIG_CHECKS
|
|
@@ -4,11 +4,7 @@ module SolidQueueGuard
|
|
|
4
4
|
module Checks
|
|
5
5
|
module Runtime
|
|
6
6
|
class Base < Checks::Base
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def call
|
|
10
|
-
skip(check_id, NOT_IMPLEMENTED_MESSAGE)
|
|
11
|
-
end
|
|
7
|
+
include DatabaseSupport
|
|
12
8
|
|
|
13
9
|
private
|
|
14
10
|
|
|
@@ -3,7 +3,26 @@
|
|
|
3
3
|
module SolidQueueGuard
|
|
4
4
|
module Checks
|
|
5
5
|
module Runtime
|
|
6
|
-
class BlockedJobsCheck < Base
|
|
6
|
+
class BlockedJobsCheck < Base
|
|
7
|
+
def call
|
|
8
|
+
with_queue_database do
|
|
9
|
+
blocked = SolidQueue::BlockedExecution.count
|
|
10
|
+
expired = SolidQueue::BlockedExecution.expired.count
|
|
11
|
+
|
|
12
|
+
if expired.positive?
|
|
13
|
+
warn(
|
|
14
|
+
check_id,
|
|
15
|
+
"#{expired} blocked job(s) have expired concurrency locks",
|
|
16
|
+
suggestion: 'Verify concurrency release maintenance is running'
|
|
17
|
+
)
|
|
18
|
+
elsif blocked.positive?
|
|
19
|
+
pass(check_id, "#{blocked} job(s) blocked by concurrency control")
|
|
20
|
+
else
|
|
21
|
+
pass(check_id, 'No blocked jobs')
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
7
26
|
end
|
|
8
27
|
end
|
|
9
28
|
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueueGuard
|
|
4
|
+
module Checks
|
|
5
|
+
module Runtime
|
|
6
|
+
module DatabaseSupport
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def queue_database_available?
|
|
10
|
+
pool = SolidQueue::Record.connection_pool
|
|
11
|
+
return false if pool.nil?
|
|
12
|
+
|
|
13
|
+
pool.with_connection(&:active?)
|
|
14
|
+
rescue StandardError
|
|
15
|
+
false
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def with_queue_database
|
|
19
|
+
return skip(check_id, 'Queue database is not available') unless queue_database_available?
|
|
20
|
+
|
|
21
|
+
yield
|
|
22
|
+
rescue StandardError => e
|
|
23
|
+
skip(check_id, "Queue database query failed: #{e.class}")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def config
|
|
27
|
+
SolidQueueGuard.config
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def lag_threshold_for(queue_name)
|
|
31
|
+
thresholds = config.check_setting(:queue_lag, :thresholds, config.queue_lag_thresholds)
|
|
32
|
+
per_queue = thresholds[queue_name.to_sym] || thresholds[queue_name]
|
|
33
|
+
return per_queue if per_queue
|
|
34
|
+
|
|
35
|
+
config.check_setting(:queue_lag, :threshold, thresholds[:default])
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -3,7 +3,32 @@
|
|
|
3
3
|
module SolidQueueGuard
|
|
4
4
|
module Checks
|
|
5
5
|
module Runtime
|
|
6
|
-
class DispatcherCheck < Base
|
|
6
|
+
class DispatcherCheck < Base
|
|
7
|
+
def call
|
|
8
|
+
with_queue_database do
|
|
9
|
+
dispatchers = SolidQueue::Process.where(kind: 'Dispatcher')
|
|
10
|
+
due_count = SolidQueue::ScheduledExecution.due.count
|
|
11
|
+
|
|
12
|
+
threshold = config.check_setting(:scheduled_backlog, :threshold, config.scheduled_backlog_threshold)
|
|
13
|
+
|
|
14
|
+
if dispatchers.none? && due_count.positive?
|
|
15
|
+
failure(
|
|
16
|
+
check_id,
|
|
17
|
+
"#{due_count} scheduled job(s) are due but no dispatcher is running",
|
|
18
|
+
suggestion: 'Start a dispatcher process or verify bin/jobs is running'
|
|
19
|
+
)
|
|
20
|
+
elsif due_count > threshold
|
|
21
|
+
warn(
|
|
22
|
+
check_id,
|
|
23
|
+
"#{due_count} scheduled executions are due (threshold: #{threshold})",
|
|
24
|
+
suggestion: 'Verify the dispatcher is keeping up with scheduled work'
|
|
25
|
+
)
|
|
26
|
+
else
|
|
27
|
+
pass(check_id, "Dispatcher healthy (#{due_count} due scheduled executions)")
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
7
32
|
end
|
|
8
33
|
end
|
|
9
34
|
end
|
|
@@ -3,7 +3,27 @@
|
|
|
3
3
|
module SolidQueueGuard
|
|
4
4
|
module Checks
|
|
5
5
|
module Runtime
|
|
6
|
-
class FailedJobsCheck < Base
|
|
6
|
+
class FailedJobsCheck < Base
|
|
7
|
+
WINDOW = 1.hour
|
|
8
|
+
|
|
9
|
+
def call
|
|
10
|
+
with_queue_database do
|
|
11
|
+
count = SolidQueue::FailedExecution.where(created_at: WINDOW.ago..).count
|
|
12
|
+
threshold = config.check_setting(:failed_jobs, :threshold, config.failed_jobs_threshold)
|
|
13
|
+
|
|
14
|
+
if count > threshold
|
|
15
|
+
warn(
|
|
16
|
+
check_id,
|
|
17
|
+
"#{count} failed job(s) in the last hour (threshold: #{threshold})",
|
|
18
|
+
suggestion: 'Review failed jobs in Mission Control or solid_queue_failed_executions',
|
|
19
|
+
metadata: { failed_jobs_last_hour: count }
|
|
20
|
+
)
|
|
21
|
+
else
|
|
22
|
+
pass(check_id, "#{count} failed job(s) in the last hour")
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
7
27
|
end
|
|
8
28
|
end
|
|
9
29
|
end
|
|
@@ -3,7 +3,26 @@
|
|
|
3
3
|
module SolidQueueGuard
|
|
4
4
|
module Checks
|
|
5
5
|
module Runtime
|
|
6
|
-
class FinishedJobsGrowthCheck < Base
|
|
6
|
+
class FinishedJobsGrowthCheck < Base
|
|
7
|
+
WINDOW = 24.hours
|
|
8
|
+
GROWTH_THRESHOLD = 10_000
|
|
9
|
+
|
|
10
|
+
def call
|
|
11
|
+
with_queue_database do
|
|
12
|
+
recent_finished = SolidQueue::Job.where(finished_at: WINDOW.ago..).count
|
|
13
|
+
|
|
14
|
+
if recent_finished > GROWTH_THRESHOLD
|
|
15
|
+
warn(
|
|
16
|
+
check_id,
|
|
17
|
+
"#{recent_finished} finished jobs in the last 24 hours",
|
|
18
|
+
suggestion: 'Ensure a recurring cleanup task runs (SolidQueue::Job.clear_finished_in_batches)'
|
|
19
|
+
)
|
|
20
|
+
else
|
|
21
|
+
pass(check_id, "#{recent_finished} finished jobs in the last 24 hours")
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
7
26
|
end
|
|
8
27
|
end
|
|
9
28
|
end
|
|
@@ -3,7 +3,24 @@
|
|
|
3
3
|
module SolidQueueGuard
|
|
4
4
|
module Checks
|
|
5
5
|
module Runtime
|
|
6
|
-
class OrphanedClaimsCheck < Base
|
|
6
|
+
class OrphanedClaimsCheck < Base
|
|
7
|
+
def call
|
|
8
|
+
with_queue_database do
|
|
9
|
+
alive_process_ids = SolidQueue::Process.pluck(:id)
|
|
10
|
+
orphaned = SolidQueue::ClaimedExecution.where.not(process_id: alive_process_ids)
|
|
11
|
+
|
|
12
|
+
if orphaned.none?
|
|
13
|
+
pass(check_id, 'No orphaned claimed executions')
|
|
14
|
+
else
|
|
15
|
+
warn(
|
|
16
|
+
check_id,
|
|
17
|
+
"#{orphaned.count} claimed execution(s) belong to dead processes",
|
|
18
|
+
suggestion: 'Restart workers; Solid Queue maintenance should release orphaned claims'
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
7
24
|
end
|
|
8
25
|
end
|
|
9
26
|
end
|
|
@@ -3,7 +3,28 @@
|
|
|
3
3
|
module SolidQueueGuard
|
|
4
4
|
module Checks
|
|
5
5
|
module Runtime
|
|
6
|
-
class PausedQueueLagCheck < Base
|
|
6
|
+
class PausedQueueLagCheck < Base
|
|
7
|
+
def call
|
|
8
|
+
with_queue_database do
|
|
9
|
+
paused_queues = SolidQueue::Pause.pluck(:queue_name)
|
|
10
|
+
return pass(check_id, 'No paused queues') if paused_queues.empty?
|
|
11
|
+
|
|
12
|
+
growing = paused_queues.select do |queue_name|
|
|
13
|
+
SolidQueue::ReadyExecution.exists?(queue_name: queue_name)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
if growing.any?
|
|
17
|
+
warn(
|
|
18
|
+
check_id,
|
|
19
|
+
"Paused queue(s) still have ready jobs: #{growing.join(', ')}",
|
|
20
|
+
suggestion: 'Unpause queues or drain jobs before pausing'
|
|
21
|
+
)
|
|
22
|
+
else
|
|
23
|
+
pass(check_id, "Paused queues have no ready backlog: #{paused_queues.join(', ')}")
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
7
28
|
end
|
|
8
29
|
end
|
|
9
30
|
end
|
|
@@ -3,7 +3,39 @@
|
|
|
3
3
|
module SolidQueueGuard
|
|
4
4
|
module Checks
|
|
5
5
|
module Runtime
|
|
6
|
-
class PidfileCheck < Base
|
|
6
|
+
class PidfileCheck < Base
|
|
7
|
+
def call
|
|
8
|
+
pidfile = ENV['SOLID_QUEUE_PIDFILE'] || default_pidfile
|
|
9
|
+
return pass(check_id, 'No Solid Queue pidfile configured') if pidfile.blank?
|
|
10
|
+
|
|
11
|
+
path = Pathname(pidfile)
|
|
12
|
+
unless path.exist?
|
|
13
|
+
return warn(check_id, "Pidfile not found at #{path}",
|
|
14
|
+
suggestion: 'Verify Solid Queue supervisor is running')
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
pid = path.read.strip.to_i
|
|
18
|
+
if process_alive?(pid)
|
|
19
|
+
pass(check_id, "Solid Queue supervisor pidfile present (pid #{pid})")
|
|
20
|
+
else
|
|
21
|
+
failure(check_id, "Pidfile exists but process #{pid} is not running",
|
|
22
|
+
suggestion: 'Restart the Solid Queue supervisor')
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def default_pidfile
|
|
29
|
+
rails_root.join('tmp/pids/solid_queue.pid').to_s
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def process_alive?(pid)
|
|
33
|
+
Process.getpgid(pid)
|
|
34
|
+
true
|
|
35
|
+
rescue Errno::ESRCH
|
|
36
|
+
false
|
|
37
|
+
end
|
|
38
|
+
end
|
|
7
39
|
end
|
|
8
40
|
end
|
|
9
41
|
end
|
|
@@ -3,7 +3,37 @@
|
|
|
3
3
|
module SolidQueueGuard
|
|
4
4
|
module Checks
|
|
5
5
|
module Runtime
|
|
6
|
-
class ProcessTopologyCheck < Base
|
|
6
|
+
class ProcessTopologyCheck < Base
|
|
7
|
+
EXPECTED_KINDS = %w[Supervisor Worker Dispatcher Scheduler].freeze
|
|
8
|
+
|
|
9
|
+
def call
|
|
10
|
+
with_queue_database do
|
|
11
|
+
kinds = SolidQueue::Process.distinct.pluck(:kind)
|
|
12
|
+
missing = EXPECTED_KINDS - kinds
|
|
13
|
+
|
|
14
|
+
if missing == EXPECTED_KINDS
|
|
15
|
+
warn(check_id, 'No Solid Queue processes are running',
|
|
16
|
+
suggestion: 'Start bin/jobs or the Solid Queue supervisor')
|
|
17
|
+
elsif missing.include?('Worker')
|
|
18
|
+
warn(check_id, 'No worker processes detected',
|
|
19
|
+
suggestion: 'Ensure workers are configured in queue.yml and running')
|
|
20
|
+
elsif missing.include?('Dispatcher') && scheduled_work_expected?
|
|
21
|
+
warn(check_id, 'No dispatcher process detected with scheduled work configured')
|
|
22
|
+
else
|
|
23
|
+
pass(check_id, "Solid Queue processes running: #{kinds.sort.join(', ')}")
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def scheduled_work_expected?
|
|
31
|
+
solid_queue_configuration.send(:recurring_tasks).any? ||
|
|
32
|
+
SolidQueue::ScheduledExecution.exists?
|
|
33
|
+
rescue StandardError
|
|
34
|
+
false
|
|
35
|
+
end
|
|
36
|
+
end
|
|
7
37
|
end
|
|
8
38
|
end
|
|
9
39
|
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueueGuard
|
|
4
|
+
module Checks
|
|
5
|
+
module Runtime
|
|
6
|
+
class PumaPluginRuntimeCheck < Base
|
|
7
|
+
def call
|
|
8
|
+
return skip(check_id, 'Solid Queue Puma plugin is not enabled') unless PumaPluginSupport.puma_plugin_enabled?
|
|
9
|
+
|
|
10
|
+
with_queue_database do
|
|
11
|
+
threshold = config.check_setting(:puma_plugin_runtime, :threshold, config.stale_process_threshold)
|
|
12
|
+
active = SolidQueue::Process.where(last_heartbeat_at: threshold.ago..)
|
|
13
|
+
|
|
14
|
+
if active.any?
|
|
15
|
+
pass(
|
|
16
|
+
check_id,
|
|
17
|
+
"Solid Queue processes are active via Puma plugin (#{active.count} process(es))",
|
|
18
|
+
metadata: { active_processes: active.count, supervisor_mode: supervisor_mode_label }
|
|
19
|
+
)
|
|
20
|
+
else
|
|
21
|
+
failure(
|
|
22
|
+
check_id,
|
|
23
|
+
'Solid Queue Puma plugin is enabled but no active processes were found',
|
|
24
|
+
suggestion: 'Verify the Puma plugin booted Solid Queue and workers are processing jobs',
|
|
25
|
+
metadata: { supervisor_mode: supervisor_mode_label }
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def supervisor_mode_label
|
|
34
|
+
PumaPluginSupport.async_supervisor_mode? ? 'async' : 'fork'
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -3,7 +3,36 @@
|
|
|
3
3
|
module SolidQueueGuard
|
|
4
4
|
module Checks
|
|
5
5
|
module Runtime
|
|
6
|
-
class QueueLagCheck < Base
|
|
6
|
+
class QueueLagCheck < Base
|
|
7
|
+
def call
|
|
8
|
+
with_queue_database do
|
|
9
|
+
lags = SolidQueue::ReadyExecution.group(:queue_name).minimum(:created_at)
|
|
10
|
+
return pass(check_id, 'No jobs waiting in ready queues') if lags.empty?
|
|
11
|
+
|
|
12
|
+
worst = lags.map do |queue_name, oldest|
|
|
13
|
+
lag = Time.current - oldest
|
|
14
|
+
{ queue: queue_name, lag: lag, threshold: lag_threshold_for(queue_name) }
|
|
15
|
+
end.max_by { |entry| entry[:lag] }
|
|
16
|
+
|
|
17
|
+
if worst[:lag] > worst[:threshold]
|
|
18
|
+
failure(
|
|
19
|
+
check_id,
|
|
20
|
+
"#{worst[:queue]} queue lag is #{worst[:lag].to_i} seconds",
|
|
21
|
+
suggestion: "Investigate worker coverage and throughput for the #{worst[:queue]} queue",
|
|
22
|
+
metadata: { queue_lag_seconds: worst[:lag].to_i, queue_name: worst[:queue] }
|
|
23
|
+
)
|
|
24
|
+
elsif worst[:lag] > worst[:threshold] / 2
|
|
25
|
+
warn(
|
|
26
|
+
check_id,
|
|
27
|
+
"#{worst[:queue]} queue lag is #{worst[:lag].to_i} seconds",
|
|
28
|
+
metadata: { queue_lag_seconds: worst[:lag].to_i, queue_name: worst[:queue] }
|
|
29
|
+
)
|
|
30
|
+
else
|
|
31
|
+
pass(check_id, "Queue lag within thresholds (worst: #{worst[:queue]} at #{worst[:lag].to_i}s)")
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
7
36
|
end
|
|
8
37
|
end
|
|
9
38
|
end
|
|
@@ -3,7 +3,35 @@
|
|
|
3
3
|
module SolidQueueGuard
|
|
4
4
|
module Checks
|
|
5
5
|
module Runtime
|
|
6
|
-
class RecurringStaleCheck < Base
|
|
6
|
+
class RecurringStaleCheck < Base
|
|
7
|
+
def call
|
|
8
|
+
with_queue_database do
|
|
9
|
+
tasks = SolidQueue::RecurringTask.static
|
|
10
|
+
return pass(check_id, 'No recurring tasks configured') if tasks.none?
|
|
11
|
+
|
|
12
|
+
stale_tasks = tasks.select do |task|
|
|
13
|
+
last_run = SolidQueue::RecurringExecution.where(task_key: task.key).maximum(:run_at)
|
|
14
|
+
last_run.nil? || last_run < expected_staleness.ago
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
if stale_tasks.any?
|
|
18
|
+
warn(
|
|
19
|
+
check_id,
|
|
20
|
+
"#{stale_tasks.size} recurring task(s) may be stale: #{stale_tasks.map(&:key).join(', ')}",
|
|
21
|
+
suggestion: 'Verify the scheduler process is running'
|
|
22
|
+
)
|
|
23
|
+
else
|
|
24
|
+
pass(check_id, 'Recurring tasks have recent executions')
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def expected_staleness
|
|
32
|
+
config.check_setting(:recurring_stale, :threshold, config.stale_process_threshold * 2)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
7
35
|
end
|
|
8
36
|
end
|
|
9
37
|
end
|
|
@@ -3,7 +3,24 @@
|
|
|
3
3
|
module SolidQueueGuard
|
|
4
4
|
module Checks
|
|
5
5
|
module Runtime
|
|
6
|
-
class ScheduledBacklogCheck < Base
|
|
6
|
+
class ScheduledBacklogCheck < Base
|
|
7
|
+
def call
|
|
8
|
+
with_queue_database do
|
|
9
|
+
due_count = SolidQueue::ScheduledExecution.due.count
|
|
10
|
+
threshold = config.check_setting(:scheduled_backlog, :threshold, config.scheduled_backlog_threshold)
|
|
11
|
+
|
|
12
|
+
if due_count > threshold
|
|
13
|
+
warn(
|
|
14
|
+
check_id,
|
|
15
|
+
"Scheduled backlog is #{due_count} (threshold: #{threshold})",
|
|
16
|
+
suggestion: 'Check dispatcher health and scheduled job volume'
|
|
17
|
+
)
|
|
18
|
+
else
|
|
19
|
+
pass(check_id, "Scheduled backlog is #{due_count}")
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
7
24
|
end
|
|
8
25
|
end
|
|
9
26
|
end
|