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.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +12 -0
  3. data/.rubocop.yml +44 -0
  4. data/.ruby-version +1 -0
  5. data/CHANGELOG.md +81 -0
  6. data/Gemfile +8 -0
  7. data/Gemfile.lock +308 -0
  8. data/README.md +79 -14
  9. data/app/controllers/solid_queue_guard/application_controller.rb +1 -0
  10. data/app/controllers/solid_queue_guard/health_controller.rb +22 -1
  11. data/lib/generators/solid_queue_guard/install/USAGE.ci +9 -0
  12. data/lib/generators/solid_queue_guard/install/ci_generator.rb +19 -0
  13. data/lib/generators/solid_queue_guard/install/templates/solid_queue_guard.rb +11 -1
  14. data/lib/generators/solid_queue_guard/install/templates/solid_queue_guard.yml +25 -0
  15. data/lib/solid_queue_guard/checks/base.rb +16 -0
  16. data/lib/solid_queue_guard/checks/config/async_supervisor_config_check.rb +25 -0
  17. data/lib/solid_queue_guard/checks/config/puma_colocated_check.rb +5 -11
  18. data/lib/solid_queue_guard/checks/config/topology_recommendation_check.rb +24 -0
  19. data/lib/solid_queue_guard/checks/registry.rb +9 -2
  20. data/lib/solid_queue_guard/checks/runtime/base.rb +1 -5
  21. data/lib/solid_queue_guard/checks/runtime/blocked_jobs_check.rb +20 -1
  22. data/lib/solid_queue_guard/checks/runtime/database_support.rb +40 -0
  23. data/lib/solid_queue_guard/checks/runtime/dispatcher_check.rb +26 -1
  24. data/lib/solid_queue_guard/checks/runtime/failed_jobs_check.rb +21 -1
  25. data/lib/solid_queue_guard/checks/runtime/finished_jobs_growth_check.rb +20 -1
  26. data/lib/solid_queue_guard/checks/runtime/orphaned_claims_check.rb +18 -1
  27. data/lib/solid_queue_guard/checks/runtime/paused_queue_lag_check.rb +22 -1
  28. data/lib/solid_queue_guard/checks/runtime/pidfile_check.rb +33 -1
  29. data/lib/solid_queue_guard/checks/runtime/process_topology_check.rb +31 -1
  30. data/lib/solid_queue_guard/checks/runtime/puma_plugin_runtime_check.rb +39 -0
  31. data/lib/solid_queue_guard/checks/runtime/queue_lag_check.rb +30 -1
  32. data/lib/solid_queue_guard/checks/runtime/recurring_stale_check.rb +29 -1
  33. data/lib/solid_queue_guard/checks/runtime/scheduled_backlog_check.rb +18 -1
  34. data/lib/solid_queue_guard/checks/runtime/stale_process_check.rb +19 -1
  35. data/lib/solid_queue_guard/cli.rb +10 -1
  36. data/lib/solid_queue_guard/configuration.rb +36 -1
  37. data/lib/solid_queue_guard/engine.rb +12 -0
  38. data/lib/solid_queue_guard/formatters/terminal.rb +1 -1
  39. data/lib/solid_queue_guard/health/cache.rb +28 -0
  40. data/lib/solid_queue_guard/health/payload.rb +50 -0
  41. data/lib/solid_queue_guard/http_status_policy.rb +35 -0
  42. data/lib/solid_queue_guard/metrics/exporter.rb +34 -0
  43. data/lib/solid_queue_guard/metrics/open_telemetry.rb +19 -0
  44. data/lib/solid_queue_guard/metrics/prometheus.rb +21 -0
  45. data/lib/solid_queue_guard/metrics/statsd.rb +25 -0
  46. data/lib/solid_queue_guard/notifier.rb +76 -0
  47. data/lib/solid_queue_guard/notifiers/base.rb +2 -7
  48. data/lib/solid_queue_guard/puma_plugin_support.rb +35 -0
  49. data/lib/solid_queue_guard/recommendations/topology.rb +76 -0
  50. data/lib/solid_queue_guard/runner.rb +10 -1
  51. data/lib/solid_queue_guard/tasks.rb +1 -1
  52. data/lib/solid_queue_guard/version.rb +1 -1
  53. data/lib/solid_queue_guard.rb +1 -1
  54. data/solid_queue_guard.gemspec +54 -0
  55. 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.notify_with = [ :rails_logger ]
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
- puma_config = rails_root.join('config/puma.rb')
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 content.match?(PUMA_PLUGIN_PATTERN)
10
+ if PumaPluginSupport.puma_plugin_enabled?
17
11
  if Rails.env.production?
18
12
  warn(
19
- 'puma_colocated',
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('puma_colocated', 'Solid Queue Puma plugin detected (non-production environment)')
18
+ pass(check_id, 'Solid Queue Puma plugin detected (non-production environment)')
25
19
  end
26
20
  else
27
- pass('puma_colocated', 'Solid Queue is not co-located with Puma')
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
- NOT_IMPLEMENTED_MESSAGE = 'Runtime check not implemented until v0.2'
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; end
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; end
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; end
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; end
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; end
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; end
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; end
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; end
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; end
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; end
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; end
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