solid_queue_guard 0.1.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/MIT-LICENSE +21 -0
- data/README.md +258 -0
- data/Rakefile +17 -0
- data/app/controllers/solid_queue_guard/application_controller.rb +6 -0
- data/app/controllers/solid_queue_guard/health_controller.rb +9 -0
- data/config/routes.rb +5 -0
- data/lib/generators/solid_queue_guard/install/USAGE +8 -0
- data/lib/generators/solid_queue_guard/install/install_generator.rb +17 -0
- data/lib/generators/solid_queue_guard/install/templates/solid_queue_guard.rb +17 -0
- data/lib/solid_queue_guard/check/result.rb +37 -0
- data/lib/solid_queue_guard/checks/base.rb +52 -0
- data/lib/solid_queue_guard/checks/config/adapter_check.rb +23 -0
- data/lib/solid_queue_guard/checks/config/connects_to_check.rb +38 -0
- data/lib/solid_queue_guard/checks/config/env_flags_check.rb +21 -0
- data/lib/solid_queue_guard/checks/config/process_heartbeat_config_check.rb +30 -0
- data/lib/solid_queue_guard/checks/config/puma_colocated_check.rb +33 -0
- data/lib/solid_queue_guard/checks/config/queue_database_check.rb +24 -0
- data/lib/solid_queue_guard/checks/config/queue_schema_check.rb +23 -0
- data/lib/solid_queue_guard/checks/config/scheduler_config_check.rb +32 -0
- data/lib/solid_queue_guard/checks/config/thread_pool_check.rb +38 -0
- data/lib/solid_queue_guard/checks/config/worker_coverage_check.rb +51 -0
- data/lib/solid_queue_guard/checks/registry.rb +48 -0
- data/lib/solid_queue_guard/checks/runtime/base.rb +21 -0
- data/lib/solid_queue_guard/checks/runtime/blocked_jobs_check.rb +9 -0
- data/lib/solid_queue_guard/checks/runtime/dispatcher_check.rb +9 -0
- data/lib/solid_queue_guard/checks/runtime/failed_jobs_check.rb +9 -0
- data/lib/solid_queue_guard/checks/runtime/finished_jobs_growth_check.rb +9 -0
- data/lib/solid_queue_guard/checks/runtime/orphaned_claims_check.rb +9 -0
- data/lib/solid_queue_guard/checks/runtime/paused_queue_lag_check.rb +9 -0
- data/lib/solid_queue_guard/checks/runtime/pidfile_check.rb +9 -0
- data/lib/solid_queue_guard/checks/runtime/process_topology_check.rb +9 -0
- data/lib/solid_queue_guard/checks/runtime/queue_lag_check.rb +9 -0
- data/lib/solid_queue_guard/checks/runtime/recurring_stale_check.rb +9 -0
- data/lib/solid_queue_guard/checks/runtime/scheduled_backlog_check.rb +9 -0
- data/lib/solid_queue_guard/checks/runtime/stale_process_check.rb +9 -0
- data/lib/solid_queue_guard/cli.rb +55 -0
- data/lib/solid_queue_guard/configuration.rb +33 -0
- data/lib/solid_queue_guard/engine.rb +21 -0
- data/lib/solid_queue_guard/formatters/json.rb +20 -0
- data/lib/solid_queue_guard/formatters/terminal.rb +43 -0
- data/lib/solid_queue_guard/notifiers/base.rb +17 -0
- data/lib/solid_queue_guard/queue_coverage.rb +37 -0
- data/lib/solid_queue_guard/report.rb +44 -0
- data/lib/solid_queue_guard/runner.rb +47 -0
- data/lib/solid_queue_guard/tasks.rb +39 -0
- data/lib/solid_queue_guard/version.rb +5 -0
- data/lib/solid_queue_guard.rb +38 -0
- metadata +346 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueueGuard
|
|
4
|
+
module Checks
|
|
5
|
+
module Config
|
|
6
|
+
class SchedulerConfigCheck < Base
|
|
7
|
+
def call
|
|
8
|
+
recurring_tasks = solid_queue_configuration.send(:recurring_tasks)
|
|
9
|
+
scheduler_expected = solid_queue_configuration.send(:schedulers).any?
|
|
10
|
+
|
|
11
|
+
return pass('scheduler_config', 'No recurring tasks configured') if recurring_tasks.empty?
|
|
12
|
+
|
|
13
|
+
if scheduler_expected
|
|
14
|
+
pass('scheduler_config', "Recurring scheduler is configured (#{recurring_tasks.size} tasks)")
|
|
15
|
+
elsif solid_queue_configuration.send(:skip_recurring_tasks?)
|
|
16
|
+
warn(
|
|
17
|
+
'scheduler_config',
|
|
18
|
+
'Recurring tasks defined but recurring execution is skipped',
|
|
19
|
+
suggestion: 'Unset SOLID_QUEUE_SKIP_RECURRING and ensure scheduler process runs'
|
|
20
|
+
)
|
|
21
|
+
else
|
|
22
|
+
failure(
|
|
23
|
+
'scheduler_config',
|
|
24
|
+
'recurring.yml defines scheduled tasks but no scheduler is configured',
|
|
25
|
+
suggestion: 'Ensure config/recurring.yml tasks have a :schedule key and scheduler is enabled'
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueueGuard
|
|
4
|
+
module Checks
|
|
5
|
+
module Config
|
|
6
|
+
class ThreadPoolCheck < Base
|
|
7
|
+
def call
|
|
8
|
+
required_threads = solid_queue_configuration.send(:estimated_number_of_threads)
|
|
9
|
+
pool_size = SolidQueue::Record.connection_pool&.size
|
|
10
|
+
|
|
11
|
+
return skip('thread_pool', 'Queue database connection pool is not available') if pool_size.nil?
|
|
12
|
+
|
|
13
|
+
max_worker_threads = solid_queue_configuration.send(:workers_options)
|
|
14
|
+
.map do |options|
|
|
15
|
+
options.fetch(
|
|
16
|
+
:threads, 3
|
|
17
|
+
)
|
|
18
|
+
end.max || 1
|
|
19
|
+
|
|
20
|
+
if pool_size >= required_threads
|
|
21
|
+
pass(
|
|
22
|
+
'thread_pool',
|
|
23
|
+
"Worker threads: #{max_worker_threads}, queue DB pool: #{pool_size}",
|
|
24
|
+
metadata: { threads: max_worker_threads, pool: pool_size, required: required_threads }
|
|
25
|
+
)
|
|
26
|
+
else
|
|
27
|
+
failure(
|
|
28
|
+
'thread_pool',
|
|
29
|
+
"Worker threads: #{max_worker_threads}, queue DB pool: #{pool_size}",
|
|
30
|
+
suggestion: "Increase queue DB pool to at least #{required_threads} or reduce worker threads",
|
|
31
|
+
metadata: { threads: max_worker_threads, pool: pool_size, required: required_threads }
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueueGuard
|
|
4
|
+
module Checks
|
|
5
|
+
module Config
|
|
6
|
+
class WorkerCoverageCheck < Base
|
|
7
|
+
def call
|
|
8
|
+
worker_queues = worker_queue_names
|
|
9
|
+
required_queues = queues_requiring_workers
|
|
10
|
+
|
|
11
|
+
uncovered = QueueCoverage.uncovered_queues(required_queues, worker_queues)
|
|
12
|
+
|
|
13
|
+
if uncovered.empty?
|
|
14
|
+
pass('worker_coverage', 'All required queues have worker coverage')
|
|
15
|
+
else
|
|
16
|
+
failure(
|
|
17
|
+
'worker_coverage',
|
|
18
|
+
"No workers configured for: #{uncovered.join(', ')}",
|
|
19
|
+
suggestion: "Add a worker for #{uncovered.join(', ')} in config/queue.yml",
|
|
20
|
+
metadata: { uncovered_queues: uncovered, worker_queues: worker_queues }
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def worker_queue_names
|
|
28
|
+
QueueCoverage.worker_queues_from_configuration(solid_queue_configuration)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def queues_requiring_workers
|
|
32
|
+
(recurring_queues + database_queues).uniq
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def recurring_queues
|
|
36
|
+
solid_queue_configuration.recurring_tasks.filter_map(&:queue_name).uniq
|
|
37
|
+
rescue StandardError
|
|
38
|
+
[]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def database_queues
|
|
42
|
+
return [] unless SolidQueue::Record.connection_pool&.connected?
|
|
43
|
+
|
|
44
|
+
SolidQueue::Job.distinct.pluck(:queue_name)
|
|
45
|
+
rescue StandardError
|
|
46
|
+
[]
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueueGuard
|
|
4
|
+
module Checks
|
|
5
|
+
# @api private
|
|
6
|
+
class Registry
|
|
7
|
+
CONFIG_CHECKS = [
|
|
8
|
+
Config::AdapterCheck,
|
|
9
|
+
Config::QueueDatabaseCheck,
|
|
10
|
+
Config::ConnectsToCheck,
|
|
11
|
+
Config::QueueSchemaCheck,
|
|
12
|
+
Config::ThreadPoolCheck,
|
|
13
|
+
Config::WorkerCoverageCheck,
|
|
14
|
+
Config::SchedulerConfigCheck,
|
|
15
|
+
Config::EnvFlagsCheck,
|
|
16
|
+
Config::ProcessHeartbeatConfigCheck,
|
|
17
|
+
Config::PumaColocatedCheck
|
|
18
|
+
].freeze
|
|
19
|
+
|
|
20
|
+
RUNTIME_CHECKS = [
|
|
21
|
+
Runtime::QueueLagCheck,
|
|
22
|
+
Runtime::StaleProcessCheck,
|
|
23
|
+
Runtime::ProcessTopologyCheck,
|
|
24
|
+
Runtime::DispatcherCheck,
|
|
25
|
+
Runtime::ScheduledBacklogCheck,
|
|
26
|
+
Runtime::BlockedJobsCheck,
|
|
27
|
+
Runtime::OrphanedClaimsCheck,
|
|
28
|
+
Runtime::FailedJobsCheck,
|
|
29
|
+
Runtime::RecurringStaleCheck,
|
|
30
|
+
Runtime::PausedQueueLagCheck,
|
|
31
|
+
Runtime::PidfileCheck,
|
|
32
|
+
Runtime::FinishedJobsGrowthCheck
|
|
33
|
+
].freeze
|
|
34
|
+
|
|
35
|
+
class << self
|
|
36
|
+
def for(scope)
|
|
37
|
+
case scope.to_sym
|
|
38
|
+
when :config then CONFIG_CHECKS
|
|
39
|
+
when :runtime then RUNTIME_CHECKS
|
|
40
|
+
when :all then CONFIG_CHECKS + RUNTIME_CHECKS
|
|
41
|
+
else
|
|
42
|
+
raise ArgumentError, "Unknown scope: #{scope}"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueueGuard
|
|
4
|
+
module Checks
|
|
5
|
+
module Runtime
|
|
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
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def check_id
|
|
16
|
+
self.class.name.demodulize.underscore.delete_suffix('_check')
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueueGuard
|
|
4
|
+
module CLI
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def options(argv: ARGV)
|
|
8
|
+
{
|
|
9
|
+
strict: strict_flag?(argv),
|
|
10
|
+
format: format_flag(argv),
|
|
11
|
+
scope: scope_flag(argv)
|
|
12
|
+
}
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def run!(scope: nil, format: nil, strict: nil, argv: ARGV)
|
|
16
|
+
scope ||= scope_flag(argv)
|
|
17
|
+
format ||= format_flag(argv)
|
|
18
|
+
strict = strict_flag?(argv) if strict.nil?
|
|
19
|
+
|
|
20
|
+
report = Runner.new(scope: scope).run
|
|
21
|
+
output = formatter_for(format).new(report).render
|
|
22
|
+
|
|
23
|
+
Rails.logger.debug output unless output.empty?
|
|
24
|
+
|
|
25
|
+
exit report.exit_code(strict: strict)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def formatter_for(format)
|
|
29
|
+
case format.to_sym
|
|
30
|
+
when :json then Formatters::Json
|
|
31
|
+
else Formatters::Terminal
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def strict_flag?(argv)
|
|
36
|
+
SolidQueueGuard.config.strict? || argv.include?('--strict')
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def format_flag(argv)
|
|
40
|
+
if (format_argument = argv.find { |argument| argument.start_with?('--format=') })
|
|
41
|
+
format_argument.split('=', 2).last.to_sym
|
|
42
|
+
else
|
|
43
|
+
ENV.fetch('SOLID_QUEUE_GUARD_FORMAT', 'text').to_sym
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def scope_flag(argv)
|
|
48
|
+
if (scope_argument = argv.find { |argument| argument.start_with?('--scope=') })
|
|
49
|
+
scope_argument.split('=', 2).last.to_sym
|
|
50
|
+
else
|
|
51
|
+
ENV.fetch('SOLID_QUEUE_GUARD_SCOPE', 'config').to_sym
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueueGuard
|
|
4
|
+
class Configuration
|
|
5
|
+
attr_accessor :enabled,
|
|
6
|
+
:queue_lag_thresholds,
|
|
7
|
+
:failed_jobs_threshold,
|
|
8
|
+
:stale_process_threshold,
|
|
9
|
+
:health_token,
|
|
10
|
+
:strict_mode,
|
|
11
|
+
:health_cache_ttl,
|
|
12
|
+
:scheduled_backlog_threshold,
|
|
13
|
+
:integrate_rails_health,
|
|
14
|
+
:notify_with
|
|
15
|
+
|
|
16
|
+
def initialize
|
|
17
|
+
@enabled = true
|
|
18
|
+
@queue_lag_thresholds = { default: 5.minutes }
|
|
19
|
+
@failed_jobs_threshold = 20
|
|
20
|
+
@stale_process_threshold = 5.minutes
|
|
21
|
+
@health_token = nil
|
|
22
|
+
@strict_mode = false
|
|
23
|
+
@health_cache_ttl = 15.seconds
|
|
24
|
+
@scheduled_backlog_threshold = 100
|
|
25
|
+
@integrate_rails_health = false
|
|
26
|
+
@notify_with = [:rails_logger]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def strict?
|
|
30
|
+
strict_mode || ActiveModel::Type::Boolean.new.cast(ENV.fetch('SOLID_QUEUE_GUARD_STRICT', nil))
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueueGuard
|
|
4
|
+
class Engine < ::Rails::Engine
|
|
5
|
+
isolate_namespace SolidQueueGuard
|
|
6
|
+
|
|
7
|
+
rake_tasks do
|
|
8
|
+
load 'solid_queue_guard/tasks.rb'
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
config.solid_queue_guard = ActiveSupport::OrderedOptions.new
|
|
12
|
+
|
|
13
|
+
initializer 'solid_queue_guard.config' do
|
|
14
|
+
SolidQueueGuard.configure do |guard_config|
|
|
15
|
+
config.solid_queue_guard.each do |name, value|
|
|
16
|
+
guard_config.public_send("#{name}=", value)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueueGuard
|
|
4
|
+
module Formatters
|
|
5
|
+
# @api private
|
|
6
|
+
class Json
|
|
7
|
+
def initialize(report)
|
|
8
|
+
@report = report
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def render
|
|
12
|
+
JSON.pretty_generate(report.to_h)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
attr_reader :report
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueueGuard
|
|
4
|
+
module Formatters
|
|
5
|
+
# @api private
|
|
6
|
+
class Terminal
|
|
7
|
+
ICONS = { pass: '✅', warn: '⚠️', fail: '❌', skip: '⏭️' }.freeze
|
|
8
|
+
|
|
9
|
+
def initialize(report)
|
|
10
|
+
@report = report
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def render
|
|
14
|
+
[
|
|
15
|
+
'SolidQueueGuard Report',
|
|
16
|
+
'',
|
|
17
|
+
"Status: #{report.status.to_s.upcase}",
|
|
18
|
+
'',
|
|
19
|
+
'Checks:',
|
|
20
|
+
*check_lines,
|
|
21
|
+
*suggestion_lines
|
|
22
|
+
].join("\n")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
attr_reader :report
|
|
28
|
+
|
|
29
|
+
def check_lines
|
|
30
|
+
report.results.map do |result|
|
|
31
|
+
icon = ICONS.fetch(result.status, '•')
|
|
32
|
+
"#{icon} #{result.message}"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def suggestion_lines
|
|
37
|
+
return [] unless report.suggestions.any?
|
|
38
|
+
|
|
39
|
+
['', 'Suggested fixes:', *report.suggestions.map { |suggestion| "- #{suggestion}" }]
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueueGuard
|
|
4
|
+
module Notifiers
|
|
5
|
+
# @api private
|
|
6
|
+
class Base
|
|
7
|
+
def self.deliver(_report)
|
|
8
|
+
raise NotImplementedError, 'Notification adapters ship in v0.3'
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
class RailsLogger < Base; end
|
|
13
|
+
class Slack < Base; end
|
|
14
|
+
class Datadog < Base; end
|
|
15
|
+
class Webhook < Base; end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueueGuard
|
|
4
|
+
module QueueCoverage
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def worker_queues_from_configuration(configuration = SolidQueue::Configuration.new)
|
|
8
|
+
configuration.configured_processes
|
|
9
|
+
.select { |process| process.kind == :worker }
|
|
10
|
+
.flat_map { |process| parse_queues_option(process.attributes[:queues]) }
|
|
11
|
+
.uniq
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def covers_all?(worker_queues, required_queue)
|
|
15
|
+
return true if worker_queues.include?('*')
|
|
16
|
+
|
|
17
|
+
worker_queues.include?(required_queue)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def uncovered_queues(required_queues, worker_queues)
|
|
21
|
+
required_queues.reject { |queue_name| covers_all?(worker_queues, queue_name) }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def parse_queues_option(value)
|
|
25
|
+
case value
|
|
26
|
+
when '*', nil
|
|
27
|
+
['*']
|
|
28
|
+
when String
|
|
29
|
+
value.split(',').map(&:strip).reject(&:empty?)
|
|
30
|
+
when Array
|
|
31
|
+
value.map(&:to_s)
|
|
32
|
+
else
|
|
33
|
+
[]
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueueGuard
|
|
4
|
+
class Report
|
|
5
|
+
STATUSES = %i[healthy degraded unhealthy].freeze
|
|
6
|
+
|
|
7
|
+
attr_reader :results
|
|
8
|
+
|
|
9
|
+
def initialize(results)
|
|
10
|
+
@results = results
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def status
|
|
14
|
+
return :unhealthy if results.any?(&:fail?)
|
|
15
|
+
return :degraded if results.any?(&:warn?)
|
|
16
|
+
|
|
17
|
+
:healthy
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def warnings
|
|
21
|
+
results.select(&:warn?).map(&:message)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def suggestions
|
|
25
|
+
results.filter_map(&:suggestion).uniq
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def exit_code(strict: false)
|
|
29
|
+
return 1 if results.any?(&:fail?)
|
|
30
|
+
return 1 if strict && results.any?(&:warn?)
|
|
31
|
+
|
|
32
|
+
0
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def to_h
|
|
36
|
+
{
|
|
37
|
+
status: status.to_s,
|
|
38
|
+
warnings: warnings,
|
|
39
|
+
suggestions: suggestions,
|
|
40
|
+
checks: results.map(&:to_h)
|
|
41
|
+
}
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SolidQueueGuard
|
|
4
|
+
# @api private
|
|
5
|
+
class Runner
|
|
6
|
+
def initialize(scope: :config, enabled: SolidQueueGuard.enabled?)
|
|
7
|
+
@scope = scope
|
|
8
|
+
@enabled = enabled
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def run
|
|
12
|
+
return Report.new([disabled_result]) unless enabled
|
|
13
|
+
|
|
14
|
+
results = checks.map do |check_class|
|
|
15
|
+
run_check(check_class)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
Report.new(results)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
attr_reader :scope, :enabled
|
|
24
|
+
|
|
25
|
+
def checks
|
|
26
|
+
Checks::Registry.for(scope)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def run_check(check_class)
|
|
30
|
+
check_class.call
|
|
31
|
+
rescue StandardError => e
|
|
32
|
+
Check::Result.new(
|
|
33
|
+
id: check_class.name.demodulize.underscore,
|
|
34
|
+
status: :fail,
|
|
35
|
+
message: "Check raised an error: #{e.class}: #{e.message}"
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def disabled_result
|
|
40
|
+
Check::Result.new(
|
|
41
|
+
id: 'disabled',
|
|
42
|
+
status: :skip,
|
|
43
|
+
message: 'SolidQueueGuard is disabled'
|
|
44
|
+
)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace :solid_queue_guard do
|
|
4
|
+
desc 'Install Solid Queue Guard'
|
|
5
|
+
task install: :environment do
|
|
6
|
+
Rails::Command.invoke :generate, ['solid_queue_guard:install']
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
desc 'Run configuration and runtime diagnostics. Usage: doctor[json] or SOLID_QUEUE_GUARD_FORMAT=json'
|
|
10
|
+
task :doctor, [:format] => :environment do |_task, args|
|
|
11
|
+
SolidQueueGuard::CLI.run!(
|
|
12
|
+
scope: :config,
|
|
13
|
+
format: cli_format(args[:format]),
|
|
14
|
+
strict: cli_strict
|
|
15
|
+
)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
desc 'Print machine-readable health status'
|
|
19
|
+
task health: :environment do
|
|
20
|
+
SolidQueueGuard::CLI.run!(scope: :config, format: :json, strict: cli_strict)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
desc 'Print full diagnostic report. Usage: report[json]'
|
|
24
|
+
task :report, [:format] => :environment do |_task, args|
|
|
25
|
+
SolidQueueGuard::CLI.run!(
|
|
26
|
+
scope: :all,
|
|
27
|
+
format: cli_format(args[:format]),
|
|
28
|
+
strict: cli_strict
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def cli_format(argument)
|
|
33
|
+
(argument.presence || ENV.fetch('SOLID_QUEUE_GUARD_FORMAT', 'text')).to_sym
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def cli_strict
|
|
37
|
+
SolidQueueGuard.config.strict?
|
|
38
|
+
end
|
|
39
|
+
end
|