solid_queue_guard 0.5.0 → 1.0.1

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 (35) 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 +88 -0
  6. data/Gemfile +8 -0
  7. data/Gemfile.lock +308 -0
  8. data/README.md +215 -11
  9. data/app/controllers/solid_queue_guard/health_controller.rb +1 -4
  10. data/lib/generators/solid_queue_guard/install/USAGE.ci +9 -0
  11. data/lib/generators/solid_queue_guard/install/ci_generator.rb +19 -0
  12. data/lib/generators/solid_queue_guard/install/templates/solid_queue_guard.rb +6 -0
  13. data/lib/generators/solid_queue_guard/install/templates/solid_queue_guard.yml +25 -0
  14. data/lib/solid_queue_guard/checks/base.rb +16 -0
  15. data/lib/solid_queue_guard/checks/config/async_supervisor_config_check.rb +25 -0
  16. data/lib/solid_queue_guard/checks/config/puma_colocated_check.rb +5 -11
  17. data/lib/solid_queue_guard/checks/config/queue_schema_check.rb +80 -7
  18. data/lib/solid_queue_guard/checks/registry.rb +8 -2
  19. data/lib/solid_queue_guard/checks/runtime/database_support.rb +5 -2
  20. data/lib/solid_queue_guard/checks/runtime/dispatcher_check.rb +4 -2
  21. data/lib/solid_queue_guard/checks/runtime/failed_jobs_check.rb +1 -1
  22. data/lib/solid_queue_guard/checks/runtime/puma_plugin_runtime_check.rb +39 -0
  23. data/lib/solid_queue_guard/checks/runtime/recurring_stale_check.rb +1 -1
  24. data/lib/solid_queue_guard/checks/runtime/scheduled_backlog_check.rb +1 -1
  25. data/lib/solid_queue_guard/checks/runtime/stale_process_check.rb +1 -1
  26. data/lib/solid_queue_guard/configuration.rb +34 -1
  27. data/lib/solid_queue_guard/http_status_policy.rb +35 -0
  28. data/lib/solid_queue_guard/puma_plugin_support.rb +35 -0
  29. data/lib/solid_queue_guard/recommendations/topology.rb +13 -0
  30. data/lib/solid_queue_guard/runner.rb +10 -1
  31. data/lib/solid_queue_guard/schema/solid_queue_tables.rb +126 -0
  32. data/lib/solid_queue_guard/version.rb +1 -1
  33. data/lib/solid_queue_guard.rb +1 -1
  34. data/solid_queue_guard.gemspec +54 -0
  35. metadata +16 -1
@@ -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
@@ -29,7 +29,7 @@ module SolidQueueGuard
29
29
  private
30
30
 
31
31
  def expected_staleness
32
- config.stale_process_threshold * 2
32
+ config.check_setting(:recurring_stale, :threshold, config.stale_process_threshold * 2)
33
33
  end
34
34
  end
35
35
  end
@@ -7,7 +7,7 @@ module SolidQueueGuard
7
7
  def call
8
8
  with_queue_database do
9
9
  due_count = SolidQueue::ScheduledExecution.due.count
10
- threshold = config.scheduled_backlog_threshold
10
+ threshold = config.check_setting(:scheduled_backlog, :threshold, config.scheduled_backlog_threshold)
11
11
 
12
12
  if due_count > threshold
13
13
  warn(
@@ -6,7 +6,7 @@ module SolidQueueGuard
6
6
  class StaleProcessCheck < Base
7
7
  def call
8
8
  with_queue_database do
9
- threshold = config.stale_process_threshold
9
+ threshold = config.check_setting(:stale_process, :threshold, config.stale_process_threshold)
10
10
  stale = SolidQueue::Process.where(last_heartbeat_at: ...threshold.ago)
11
11
 
12
12
  if stale.none?
@@ -12,7 +12,11 @@ module SolidQueueGuard
12
12
  :scheduled_backlog_threshold,
13
13
  :integrate_rails_health,
14
14
  :notify_with,
15
- :metrics_backends
15
+ :metrics_backends,
16
+ :disabled_checks,
17
+ :checks,
18
+ :degraded_http_status,
19
+ :unhealthy_http_status
16
20
 
17
21
  def initialize
18
22
  @enabled = true
@@ -26,10 +30,39 @@ module SolidQueueGuard
26
30
  @integrate_rails_health = false
27
31
  @notify_with = [:rails_logger]
28
32
  @metrics_backends = []
33
+ @disabled_checks = []
34
+ @checks = ActiveSupport::OrderedOptions.new
35
+ @degraded_http_status = :ok
36
+ @unhealthy_http_status = :service_unavailable
29
37
  end
30
38
 
31
39
  def strict?
32
40
  strict_mode || ActiveModel::Type::Boolean.new.cast(ENV.fetch('SOLID_QUEUE_GUARD_STRICT', nil))
33
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
34
67
  end
35
68
  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,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
@@ -31,6 +31,8 @@ module SolidQueueGuard
31
31
  end
32
32
 
33
33
  def pool_recommendations
34
+ return async_pool_recommendation if PumaPluginSupport.async_supervisor_mode?
35
+
34
36
  required_threads = configuration.estimated_number_of_threads
35
37
  pool_size = SolidQueue::Record.connection_pool&.size
36
38
  return [] if pool_size.nil? || required_threads <= pool_size
@@ -58,6 +60,17 @@ module SolidQueueGuard
58
60
  rescue StandardError
59
61
  false
60
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
61
74
  end
62
75
  end
63
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: check_class.name.demodulize.underscore,
42
+ id: check_id,
34
43
  status: :fail,
35
44
  message: "Check raised an error: #{e.class}: #{e.message}"
36
45
  )
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidQueueGuard
4
+ module Schema
5
+ # @api private
6
+ class SolidQueueTables
7
+ SCHEMA_FILES = %w[
8
+ db/queue_schema.rb
9
+ db/schema.rb
10
+ db/structure.sql
11
+ ].freeze
12
+
13
+ RUBY_CREATE_TABLE_PATTERN = /
14
+ create_table
15
+ \s+
16
+ (?:
17
+ ["'](?<quoted>solid_queue_[a-z_]+)["']
18
+ |
19
+ :(?<symbol>solid_queue_[a-z_]+)
20
+ )
21
+ /ix
22
+
23
+ SQL_CREATE_TABLE_PATTERN = /
24
+ CREATE \ TABLE
25
+ (?: \ IF \ NOT \ EXISTS )?
26
+ \s+
27
+ (?:[\w.]+\.)?
28
+ (?:
29
+ "(?<quoted>solid_queue_[a-z_]+)"
30
+ |
31
+ (?<unquoted>solid_queue_[a-z_]+)
32
+ )
33
+ /ix
34
+
35
+ class << self
36
+ def required_tables
37
+ @required_tables ||= tables_from_gem_template || default_required_tables
38
+ end
39
+
40
+ def detected_tables(rails_root: Rails.root)
41
+ file_tables = {}
42
+ tables = Set.new
43
+
44
+ SCHEMA_FILES.each do |relative_path|
45
+ path = rails_root.join(relative_path)
46
+ next unless path.exist?
47
+
48
+ tables_in_file = parse_file(path)
49
+ next if tables_in_file.empty?
50
+
51
+ file_tables[relative_path] = tables_in_file.sort
52
+ tables.merge(tables_in_file)
53
+ end
54
+
55
+ database_tables = tables_in_queue_database
56
+ tables.merge(database_tables)
57
+
58
+ {
59
+ tables: tables.sort,
60
+ file_tables: file_tables,
61
+ database_tables: database_tables.sort
62
+ }
63
+ end
64
+
65
+ def missing_tables(rails_root: Rails.root)
66
+ required_tables - detected_tables(rails_root: rails_root)[:tables]
67
+ end
68
+
69
+ def tables_from_gem_template
70
+ spec = Gem.loaded_specs['solid_queue']
71
+ return nil unless spec
72
+
73
+ template = File.join(
74
+ spec.full_gem_path,
75
+ 'lib/generators/solid_queue/install/templates/db/queue_schema.rb'
76
+ )
77
+ return nil unless File.exist?(template)
78
+
79
+ parse_ruby_schema(File.read(template))
80
+ end
81
+
82
+ def default_required_tables
83
+ %w[
84
+ solid_queue_blocked_executions
85
+ solid_queue_claimed_executions
86
+ solid_queue_failed_executions
87
+ solid_queue_jobs
88
+ solid_queue_pauses
89
+ solid_queue_processes
90
+ solid_queue_ready_executions
91
+ solid_queue_recurring_executions
92
+ solid_queue_recurring_tasks
93
+ solid_queue_scheduled_executions
94
+ solid_queue_semaphores
95
+ ].freeze
96
+ end
97
+
98
+ private
99
+
100
+ def parse_file(path)
101
+ content = path.read
102
+ path.extname == '.sql' ? parse_sql_schema(content) : parse_ruby_schema(content)
103
+ end
104
+
105
+ def parse_ruby_schema(content)
106
+ content.scan(RUBY_CREATE_TABLE_PATTERN).flatten.compact.uniq
107
+ end
108
+
109
+ def parse_sql_schema(content)
110
+ content.scan(SQL_CREATE_TABLE_PATTERN).flatten.compact.uniq
111
+ end
112
+
113
+ def tables_in_queue_database
114
+ return [] unless defined?(SolidQueue::Record)
115
+
116
+ pool = SolidQueue::Record.connection_pool
117
+ return [] unless pool&.with_connection(&:active?)
118
+
119
+ SolidQueue::Record.connection.tables.grep(/\Asolid_queue_/)
120
+ rescue StandardError
121
+ []
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SolidQueueGuard
4
- VERSION = '0.5.0'
4
+ VERSION = '1.0.1'
5
5
  end
@@ -18,7 +18,7 @@ module SolidQueueGuard
18
18
  attr_accessor :configuration
19
19
 
20
20
  def deprecator
21
- @deprecator ||= ActiveSupport::Deprecation.new('0.2', 'SolidQueueGuard')
21
+ @deprecator ||= ActiveSupport::Deprecation.new('2.0', 'SolidQueueGuard')
22
22
  end
23
23
 
24
24
  def configure
@@ -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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: solid_queue_guard
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 1.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rafael Pissardo
@@ -269,6 +269,12 @@ executables: []
269
269
  extensions: []
270
270
  extra_rdoc_files: []
271
271
  files:
272
+ - ".gitignore"
273
+ - ".rubocop.yml"
274
+ - ".ruby-version"
275
+ - CHANGELOG.md
276
+ - Gemfile
277
+ - Gemfile.lock
272
278
  - MIT-LICENSE
273
279
  - README.md
274
280
  - Rakefile
@@ -276,12 +282,16 @@ files:
276
282
  - app/controllers/solid_queue_guard/health_controller.rb
277
283
  - config/routes.rb
278
284
  - lib/generators/solid_queue_guard/install/USAGE
285
+ - lib/generators/solid_queue_guard/install/USAGE.ci
286
+ - lib/generators/solid_queue_guard/install/ci_generator.rb
279
287
  - lib/generators/solid_queue_guard/install/install_generator.rb
280
288
  - lib/generators/solid_queue_guard/install/templates/solid_queue_guard.rb
289
+ - lib/generators/solid_queue_guard/install/templates/solid_queue_guard.yml
281
290
  - lib/solid_queue_guard.rb
282
291
  - lib/solid_queue_guard/check/result.rb
283
292
  - lib/solid_queue_guard/checks/base.rb
284
293
  - lib/solid_queue_guard/checks/config/adapter_check.rb
294
+ - lib/solid_queue_guard/checks/config/async_supervisor_config_check.rb
285
295
  - lib/solid_queue_guard/checks/config/connects_to_check.rb
286
296
  - lib/solid_queue_guard/checks/config/env_flags_check.rb
287
297
  - lib/solid_queue_guard/checks/config/process_heartbeat_config_check.rb
@@ -303,6 +313,7 @@ files:
303
313
  - lib/solid_queue_guard/checks/runtime/paused_queue_lag_check.rb
304
314
  - lib/solid_queue_guard/checks/runtime/pidfile_check.rb
305
315
  - lib/solid_queue_guard/checks/runtime/process_topology_check.rb
316
+ - lib/solid_queue_guard/checks/runtime/puma_plugin_runtime_check.rb
306
317
  - lib/solid_queue_guard/checks/runtime/queue_lag_check.rb
307
318
  - lib/solid_queue_guard/checks/runtime/recurring_stale_check.rb
308
319
  - lib/solid_queue_guard/checks/runtime/scheduled_backlog_check.rb
@@ -314,18 +325,22 @@ files:
314
325
  - lib/solid_queue_guard/formatters/terminal.rb
315
326
  - lib/solid_queue_guard/health/cache.rb
316
327
  - lib/solid_queue_guard/health/payload.rb
328
+ - lib/solid_queue_guard/http_status_policy.rb
317
329
  - lib/solid_queue_guard/metrics/exporter.rb
318
330
  - lib/solid_queue_guard/metrics/open_telemetry.rb
319
331
  - lib/solid_queue_guard/metrics/prometheus.rb
320
332
  - lib/solid_queue_guard/metrics/statsd.rb
321
333
  - lib/solid_queue_guard/notifier.rb
322
334
  - lib/solid_queue_guard/notifiers/base.rb
335
+ - lib/solid_queue_guard/puma_plugin_support.rb
323
336
  - lib/solid_queue_guard/queue_coverage.rb
324
337
  - lib/solid_queue_guard/recommendations/topology.rb
325
338
  - lib/solid_queue_guard/report.rb
326
339
  - lib/solid_queue_guard/runner.rb
340
+ - lib/solid_queue_guard/schema/solid_queue_tables.rb
327
341
  - lib/solid_queue_guard/tasks.rb
328
342
  - lib/solid_queue_guard/version.rb
343
+ - solid_queue_guard.gemspec
329
344
  homepage: https://github.com/rafael-pissardo/solid_queue_guard
330
345
  licenses:
331
346
  - MIT