solid_queue_autoscaler 1.0.7 → 1.0.9
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/CHANGELOG.md +45 -0
- data/README.md +770 -4
- data/lib/generators/solid_queue_autoscaler/migration_generator.rb +180 -4
- data/lib/generators/solid_queue_autoscaler/templates/README +27 -6
- data/lib/generators/solid_queue_autoscaler/templates/create_solid_queue_autoscaler_events.rb.erb +17 -10
- data/lib/generators/solid_queue_autoscaler/templates/create_solid_queue_autoscaler_state.rb.erb +9 -0
- data/lib/generators/solid_queue_autoscaler/templates/initializer.rb +6 -0
- data/lib/solid_queue_autoscaler/autoscale_job.rb +10 -0
- data/lib/solid_queue_autoscaler/configuration.rb +13 -0
- data/lib/solid_queue_autoscaler/scale_event.rb +183 -0
- data/lib/solid_queue_autoscaler/scaler.rb +98 -20
- data/lib/solid_queue_autoscaler/version.rb +1 -1
- data/lib/solid_queue_autoscaler.rb +254 -0
- metadata +16 -2
|
@@ -10,19 +10,195 @@ module SolidQueueAutoscaler
|
|
|
10
10
|
|
|
11
11
|
source_root File.expand_path('templates', __dir__)
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
class_option :database, type: :string, default: nil,
|
|
14
|
+
desc: 'Specify database for multi-database setups (e.g., --database=queue)'
|
|
14
15
|
|
|
15
|
-
|
|
16
|
+
desc 'Creates migrations for SolidQueueAutoscaler tables'
|
|
17
|
+
|
|
18
|
+
def create_migration_files
|
|
19
|
+
detected_config = detect_database_config
|
|
20
|
+
migration_dir = determine_migration_directory(detected_config)
|
|
21
|
+
db_name = effective_database_name(detected_config)
|
|
22
|
+
|
|
23
|
+
# Show what we detected
|
|
24
|
+
print_detection_info(detected_config, migration_dir)
|
|
25
|
+
|
|
26
|
+
# Create state table migration
|
|
16
27
|
migration_template 'create_solid_queue_autoscaler_state.rb.erb',
|
|
17
|
-
|
|
28
|
+
"#{migration_dir}/create_solid_queue_autoscaler_state.rb"
|
|
29
|
+
|
|
30
|
+
# Create events table migration
|
|
31
|
+
migration_template 'create_solid_queue_autoscaler_events.rb.erb',
|
|
32
|
+
"#{migration_dir}/create_solid_queue_autoscaler_events.rb"
|
|
33
|
+
|
|
34
|
+
print_instructions(migration_dir, db_name)
|
|
18
35
|
end
|
|
19
36
|
|
|
20
37
|
private
|
|
21
38
|
|
|
39
|
+
# Detect database configuration for SolidQueue.
|
|
40
|
+
# Returns a hash with :database_name, :migrations_path, :is_multi_db.
|
|
41
|
+
def detect_database_config
|
|
42
|
+
result = { database_name: nil, migrations_path: nil, is_multi_db: false }
|
|
43
|
+
|
|
44
|
+
# Check if Rails database config is available
|
|
45
|
+
return result unless defined?(Rails) && Rails.application
|
|
46
|
+
|
|
47
|
+
db_configs = Rails.application.config.database_configuration
|
|
48
|
+
env_config = db_configs[Rails.env.to_s] || {}
|
|
49
|
+
|
|
50
|
+
# Look for a 'queue' database configuration (Rails multi-DB naming convention).
|
|
51
|
+
# SolidQueue typically uses 'queue' as the database name.
|
|
52
|
+
queue_config = find_queue_database_config(env_config)
|
|
53
|
+
|
|
54
|
+
if queue_config
|
|
55
|
+
result[:is_multi_db] = true
|
|
56
|
+
result[:database_name] = queue_config[:name]
|
|
57
|
+
result[:migrations_path] = queue_config[:migrations_path]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
result
|
|
61
|
+
rescue StandardError => e
|
|
62
|
+
say ' (Could not detect database config: ' + e.message + ')', :yellow
|
|
63
|
+
{ database_name: nil, migrations_path: nil, is_multi_db: false }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Find the queue database configuration from environment config.
|
|
67
|
+
def find_queue_database_config(env_config)
|
|
68
|
+
# Rails 7+ multi-database format: { 'primary' => {...}, 'queue' => {...} }
|
|
69
|
+
# Look for common queue database names.
|
|
70
|
+
queue_db_names = %w[queue solid_queue queue_database]
|
|
71
|
+
|
|
72
|
+
queue_db_names.each do |db_name|
|
|
73
|
+
if env_config[db_name].is_a?(Hash)
|
|
74
|
+
config = env_config[db_name]
|
|
75
|
+
migrations_path = normalize_migrations_path(config['migrations_paths'], db_name)
|
|
76
|
+
return { name: db_name, migrations_path: migrations_path }
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Check if any database config has migrations_paths containing 'queue'.
|
|
81
|
+
env_config.each do |name, config|
|
|
82
|
+
next unless config.is_a?(Hash)
|
|
83
|
+
next if name == 'primary' # Skip primary database
|
|
84
|
+
|
|
85
|
+
migrations_paths = config['migrations_paths']
|
|
86
|
+
path_str = normalize_migrations_path(migrations_paths, name)
|
|
87
|
+
if path_str.include?('queue')
|
|
88
|
+
return { name: name, migrations_path: path_str }
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
nil
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Normalize migrations_paths which can be a string or array.
|
|
96
|
+
def normalize_migrations_path(migrations_paths, db_name)
|
|
97
|
+
case migrations_paths
|
|
98
|
+
when String
|
|
99
|
+
migrations_paths
|
|
100
|
+
when Array
|
|
101
|
+
migrations_paths.first || "db/#{db_name}_migrate"
|
|
102
|
+
else
|
|
103
|
+
"db/#{db_name}_migrate"
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Determine the migration directory to use.
|
|
108
|
+
def determine_migration_directory(detected_config)
|
|
109
|
+
# 1. Explicit --database option takes precedence
|
|
110
|
+
if options[:database]
|
|
111
|
+
return 'db/' + options[:database] + '_migrate'
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# 2. If we detected a queue database with migrations_path, use it
|
|
115
|
+
if detected_config[:is_multi_db] && detected_config[:migrations_path]
|
|
116
|
+
return detected_config[:migrations_path]
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# 3. Default to standard migrate directory
|
|
120
|
+
'db/migrate'
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Get the effective database name for running migrations.
|
|
124
|
+
def effective_database_name(detected_config)
|
|
125
|
+
return options[:database] if options[:database]
|
|
126
|
+
return detected_config[:database_name] if detected_config[:is_multi_db]
|
|
127
|
+
|
|
128
|
+
nil
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def print_detection_info(detected_config, migration_dir)
|
|
132
|
+
say ''
|
|
133
|
+
|
|
134
|
+
if options[:database]
|
|
135
|
+
say '📁 Using specified database: ' + options[:database], :green
|
|
136
|
+
elsif detected_config[:is_multi_db]
|
|
137
|
+
say '📁 Auto-detected multi-database setup!', :green
|
|
138
|
+
say ' Database: ' + detected_config[:database_name].to_s, :green
|
|
139
|
+
say ' Migration path: ' + detected_config[:migrations_path].to_s, :green
|
|
140
|
+
else
|
|
141
|
+
say '📁 Using standard migration directory', :green
|
|
142
|
+
|
|
143
|
+
# Runtime check: warn if SolidQueue appears to use separate connection
|
|
144
|
+
if solidqueue_uses_separate_connection?
|
|
145
|
+
say ''
|
|
146
|
+
say '⚠️ Warning: SolidQueue appears to use a separate database connection!', :yellow
|
|
147
|
+
say ' But we could not detect the migrations_paths from database.yml.', :yellow
|
|
148
|
+
say ' If tables end up in the wrong database, re-run with:', :yellow
|
|
149
|
+
say ' rails g solid_queue_autoscaler:migration --database=queue', :yellow
|
|
150
|
+
say ''
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
say ' Placing migrations in: ' + migration_dir + '/', :blue
|
|
155
|
+
say ''
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Check at runtime if SolidQueue uses a separate database connection.
|
|
159
|
+
def solidqueue_uses_separate_connection?
|
|
160
|
+
return false unless defined?(SolidQueue::Record)
|
|
161
|
+
return false unless SolidQueue::Record.respond_to?(:connection)
|
|
162
|
+
|
|
163
|
+
sq_pool = SolidQueue::Record.connection_pool
|
|
164
|
+
ar_pool = ActiveRecord::Base.connection_pool
|
|
165
|
+
|
|
166
|
+
sq_pool.db_config.database != ar_pool.db_config.database
|
|
167
|
+
rescue StandardError
|
|
168
|
+
false
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def print_instructions(migration_dir, db_name)
|
|
172
|
+
say '✅ Migrations generated successfully!', :green
|
|
173
|
+
say ''
|
|
174
|
+
say '📖 To run the migrations:', :blue
|
|
175
|
+
|
|
176
|
+
if db_name
|
|
177
|
+
# Multi-database setup
|
|
178
|
+
say ' rails db:migrate:' + db_name.to_s, :cyan
|
|
179
|
+
say ''
|
|
180
|
+
say ' Or if that does not work:', :blue
|
|
181
|
+
say ' DATABASE=' + db_name.to_s + ' rails db:migrate', :cyan
|
|
182
|
+
elsif migration_dir != 'db/migrate'
|
|
183
|
+
# Custom migration directory without detected database name
|
|
184
|
+
db_from_path = migration_dir.sub('db/', '').sub('_migrate', '')
|
|
185
|
+
say ' rails db:migrate:' + db_from_path, :cyan
|
|
186
|
+
else
|
|
187
|
+
# Standard single-database
|
|
188
|
+
say ' rails db:migrate', :cyan
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
say ''
|
|
192
|
+
say '🔍 To verify setup after migration:', :blue
|
|
193
|
+
say ' SolidQueueAutoscaler.verify_setup!', :cyan
|
|
194
|
+
say ''
|
|
195
|
+
end
|
|
196
|
+
|
|
22
197
|
def migration_version
|
|
23
198
|
return unless defined?(ActiveRecord::VERSION)
|
|
24
199
|
|
|
25
|
-
|
|
200
|
+
'[' + ActiveRecord::VERSION::MAJOR.to_s + '.' +
|
|
201
|
+
ActiveRecord::VERSION::MINOR.to_s + ']'
|
|
26
202
|
end
|
|
27
203
|
end
|
|
28
204
|
end
|
|
@@ -5,24 +5,45 @@ SolidQueueAutoscaler has been installed!
|
|
|
5
5
|
Next steps:
|
|
6
6
|
|
|
7
7
|
1. Set environment variables:
|
|
8
|
-
- HEROKU_API_KEY: Generate with `heroku authorizations:create -d
|
|
8
|
+
- HEROKU_API_KEY: Generate with `heroku authorizations:create -d 'Solid Queue Autoscaler'`
|
|
9
9
|
- HEROKU_APP_NAME: Your Heroku app name
|
|
10
10
|
|
|
11
|
-
2. Run the migration generator
|
|
11
|
+
2. Run the migration generator:
|
|
12
12
|
|
|
13
|
+
SINGLE DATABASE (SolidQueue uses same database as your app):
|
|
14
|
+
---------------------------------------------------------------
|
|
13
15
|
rails generate solid_queue_autoscaler:migration
|
|
14
16
|
rails db:migrate
|
|
15
17
|
|
|
16
|
-
|
|
18
|
+
MULTI-DATABASE (SolidQueue in separate 'queue' database):
|
|
19
|
+
---------------------------------------------------------------
|
|
20
|
+
The generator will auto-detect your setup and place migrations
|
|
21
|
+
in the correct directory (e.g., db/queue_migrate/).
|
|
17
22
|
|
|
18
|
-
|
|
23
|
+
rails generate solid_queue_autoscaler:migration
|
|
24
|
+
rails db:migrate:queue
|
|
25
|
+
|
|
26
|
+
Or specify explicitly:
|
|
27
|
+
rails generate solid_queue_autoscaler:migration --database=queue
|
|
28
|
+
rails db:migrate:queue
|
|
29
|
+
|
|
30
|
+
3. Verify installation:
|
|
31
|
+
|
|
32
|
+
rails console
|
|
33
|
+
> SolidQueueAutoscaler.verify_setup!
|
|
34
|
+
|
|
35
|
+
This will check that tables are in the correct database.
|
|
36
|
+
|
|
37
|
+
4. Review config/initializers/solid_queue_autoscaler.rb and adjust thresholds
|
|
38
|
+
|
|
39
|
+
5. Add the recurring job to config/recurring.yml:
|
|
19
40
|
|
|
20
41
|
autoscaler:
|
|
21
42
|
class: SolidQueueAutoscaler::AutoscaleJob
|
|
22
43
|
queue: autoscaler
|
|
23
44
|
schedule: every 30 seconds
|
|
24
45
|
|
|
25
|
-
|
|
46
|
+
6. Configure a dedicated queue in config/queue.yml:
|
|
26
47
|
|
|
27
48
|
queues:
|
|
28
49
|
- autoscaler
|
|
@@ -34,7 +55,7 @@ Next steps:
|
|
|
34
55
|
- queues: [default]
|
|
35
56
|
threads: 5
|
|
36
57
|
|
|
37
|
-
|
|
58
|
+
7. Test with dry_run mode before enabling in production
|
|
38
59
|
|
|
39
60
|
For more information, see: https://github.com/reillyse/solid_queue_autoscaler
|
|
40
61
|
|
data/lib/generators/solid_queue_autoscaler/templates/create_solid_queue_autoscaler_events.rb.erb
CHANGED
|
@@ -1,24 +1,31 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# Migration for SolidQueueAutoscaler events table (for dashboard and event history).
|
|
4
|
+
#
|
|
5
|
+
# For multi-database setups (SolidQueue in separate database):
|
|
6
|
+
# This migration should be placed in db/queue_migrate/ (or your queue DB's migration path)
|
|
7
|
+
# Run with: rails db:migrate:queue
|
|
8
|
+
#
|
|
9
|
+
# For single-database setups:
|
|
10
|
+
# Place in db/migrate/ and run: rails db:migrate
|
|
11
|
+
#
|
|
3
12
|
class CreateSolidQueueAutoscalerEvents < ActiveRecord::Migration<%= migration_version %>
|
|
4
13
|
def change
|
|
5
14
|
create_table :solid_queue_autoscaler_events do |t|
|
|
6
|
-
t.string :worker_name, null: false
|
|
15
|
+
t.string :worker_name, null: false, default: 'default'
|
|
7
16
|
t.string :action, null: false
|
|
8
|
-
t.integer :from_workers, null: false
|
|
9
|
-
t.integer :to_workers, null: false
|
|
10
|
-
t.
|
|
11
|
-
t.integer :queue_depth
|
|
12
|
-
t.float :latency_seconds
|
|
13
|
-
t.
|
|
17
|
+
t.integer :from_workers, null: false
|
|
18
|
+
t.integer :to_workers, null: false
|
|
19
|
+
t.string :reason
|
|
20
|
+
t.integer :queue_depth
|
|
21
|
+
t.float :latency_seconds
|
|
22
|
+
t.text :metrics_json
|
|
14
23
|
t.boolean :dry_run, default: false
|
|
15
|
-
|
|
16
24
|
t.datetime :created_at, null: false
|
|
17
25
|
end
|
|
18
26
|
|
|
27
|
+
add_index :solid_queue_autoscaler_events, :created_at
|
|
19
28
|
add_index :solid_queue_autoscaler_events, :worker_name
|
|
20
29
|
add_index :solid_queue_autoscaler_events, :action
|
|
21
|
-
add_index :solid_queue_autoscaler_events, :created_at
|
|
22
|
-
add_index :solid_queue_autoscaler_events, %i[worker_name created_at]
|
|
23
30
|
end
|
|
24
31
|
end
|
data/lib/generators/solid_queue_autoscaler/templates/create_solid_queue_autoscaler_state.rb.erb
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# Migration for SolidQueueAutoscaler cooldown state table.
|
|
4
|
+
#
|
|
5
|
+
# For multi-database setups (SolidQueue in separate database):
|
|
6
|
+
# This migration should be placed in db/queue_migrate/ (or your queue DB's migration path)
|
|
7
|
+
# Run with: rails db:migrate:queue
|
|
8
|
+
#
|
|
9
|
+
# For single-database setups:
|
|
10
|
+
# Place in db/migrate/ and run: rails db:migrate
|
|
11
|
+
#
|
|
3
12
|
class CreateSolidQueueAutoscalerState < ActiveRecord::Migration<%= migration_version %>
|
|
4
13
|
def change
|
|
5
14
|
create_table :solid_queue_autoscaler_state do |t|
|
|
@@ -55,4 +55,10 @@ SolidQueueAutoscaler.configure do |config|
|
|
|
55
55
|
config.record_events = true
|
|
56
56
|
# Also record no_change events (verbose, generates many records)
|
|
57
57
|
# config.record_all_events = false
|
|
58
|
+
|
|
59
|
+
# AutoscaleJob Settings
|
|
60
|
+
# Queue for the autoscaler job (use a fast/high-priority Solid Queue queue)
|
|
61
|
+
config.job_queue = :autoscaler
|
|
62
|
+
# Priority for the autoscaler job (lower = higher priority, nil = default)
|
|
63
|
+
# config.job_priority = 0 # Uncomment to set highest priority
|
|
58
64
|
end
|
|
@@ -2,7 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
module SolidQueueAutoscaler
|
|
4
4
|
class AutoscaleJob < ActiveJob::Base
|
|
5
|
+
# IMPORTANT: Use a static queue name so SolidQueue recurring jobs work correctly.
|
|
6
|
+
# When using SolidQueue recurring.yml without specifying queue:, SolidQueue
|
|
7
|
+
# checks the job class's queue_name attribute. A dynamic queue_as block
|
|
8
|
+
# returns a Proc that isn't evaluated by recurring jobs, causing jobs to
|
|
9
|
+
# go to 'default' queue instead.
|
|
10
|
+
#
|
|
11
|
+
# To use a custom queue:
|
|
12
|
+
# 1. Set queue: in your recurring.yml (recommended)
|
|
13
|
+
# 2. Or use AutoscaleJob.set(queue: :my_queue).perform_later
|
|
5
14
|
queue_as :autoscaler
|
|
15
|
+
|
|
6
16
|
discard_on ConfigurationError
|
|
7
17
|
|
|
8
18
|
# Scale a specific worker type, or all workers if :all is passed
|
|
@@ -63,6 +63,12 @@ module SolidQueueAutoscaler
|
|
|
63
63
|
# Dashboard/event recording settings
|
|
64
64
|
attr_accessor :record_events, :record_all_events
|
|
65
65
|
|
|
66
|
+
# Cooldown persistence (survives process restarts)
|
|
67
|
+
attr_accessor :persist_cooldowns
|
|
68
|
+
|
|
69
|
+
# AutoscaleJob settings
|
|
70
|
+
attr_accessor :job_queue, :job_priority
|
|
71
|
+
|
|
66
72
|
def initialize
|
|
67
73
|
# Configuration name (auto-set when using named configurations)
|
|
68
74
|
@name = :default
|
|
@@ -128,6 +134,13 @@ module SolidQueueAutoscaler
|
|
|
128
134
|
# Dashboard/event recording settings
|
|
129
135
|
@record_events = true # Record scale events to database
|
|
130
136
|
@record_all_events = false # Also record no_change events (verbose)
|
|
137
|
+
|
|
138
|
+
# Cooldown persistence (survives process restarts, requires migration)
|
|
139
|
+
@persist_cooldowns = true
|
|
140
|
+
|
|
141
|
+
# AutoscaleJob settings
|
|
142
|
+
@job_queue = :autoscaler # Queue name for the autoscaler job
|
|
143
|
+
@job_priority = nil # Job priority (lower = higher priority, nil = default)
|
|
131
144
|
end
|
|
132
145
|
|
|
133
146
|
# Returns the lock key, auto-generating based on name if not explicitly set
|
|
@@ -48,6 +48,124 @@ module SolidQueueAutoscaler
|
|
|
48
48
|
end
|
|
49
49
|
|
|
50
50
|
class << self
|
|
51
|
+
# Diagnostic method to help debug why events aren't being saved.
|
|
52
|
+
# @param config [Configuration, nil] Configuration to check (uses default if nil)
|
|
53
|
+
# @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter, nil] Database connection
|
|
54
|
+
# @return [Hash] Diagnostic information
|
|
55
|
+
def debug_event_saving(config: nil, connection: nil)
|
|
56
|
+
config ||= SolidQueueAutoscaler.config rescue nil
|
|
57
|
+
conn = connection || default_connection rescue nil
|
|
58
|
+
|
|
59
|
+
diagnostics = {
|
|
60
|
+
timestamp: Time.current,
|
|
61
|
+
config_present: !config.nil?,
|
|
62
|
+
connection_present: !conn.nil?,
|
|
63
|
+
record_events_setting: nil,
|
|
64
|
+
connection_available: false,
|
|
65
|
+
record_events_effective: false,
|
|
66
|
+
table_exists: false,
|
|
67
|
+
can_query: false,
|
|
68
|
+
can_insert: false,
|
|
69
|
+
issues: [],
|
|
70
|
+
connection_class: nil,
|
|
71
|
+
error: nil
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# Check config settings
|
|
75
|
+
if config
|
|
76
|
+
diagnostics[:record_events_setting] = config.record_events
|
|
77
|
+
diagnostics[:connection_available] = config.connection_available?
|
|
78
|
+
diagnostics[:record_events_effective] = config.record_events?
|
|
79
|
+
|
|
80
|
+
unless config.record_events
|
|
81
|
+
diagnostics[:issues] << 'config.record_events is false'
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
unless config.connection_available?
|
|
85
|
+
diagnostics[:issues] << 'config.connection_available? returned false'
|
|
86
|
+
end
|
|
87
|
+
else
|
|
88
|
+
diagnostics[:issues] << 'No configuration found'
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Check connection
|
|
92
|
+
if conn
|
|
93
|
+
diagnostics[:connection_class] = conn.class.name
|
|
94
|
+
|
|
95
|
+
# Check table exists
|
|
96
|
+
begin
|
|
97
|
+
diagnostics[:table_exists] = conn.table_exists?(TABLE_NAME)
|
|
98
|
+
unless diagnostics[:table_exists]
|
|
99
|
+
diagnostics[:issues] << "Table '#{TABLE_NAME}' does not exist. Run: rails generate solid_queue_autoscaler:dashboard"
|
|
100
|
+
end
|
|
101
|
+
rescue StandardError => e
|
|
102
|
+
diagnostics[:issues] << "Error checking table existence: #{e.message}"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Check can query
|
|
106
|
+
if diagnostics[:table_exists]
|
|
107
|
+
begin
|
|
108
|
+
conn.select_value("SELECT COUNT(*) FROM #{TABLE_NAME}")
|
|
109
|
+
diagnostics[:can_query] = true
|
|
110
|
+
rescue StandardError => e
|
|
111
|
+
diagnostics[:issues] << "Error querying table: #{e.message}"
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Check can insert (with rollback)
|
|
116
|
+
if diagnostics[:can_query]
|
|
117
|
+
begin
|
|
118
|
+
# Try a test insert that we'll check but not actually commit
|
|
119
|
+
test_sql = <<~SQL
|
|
120
|
+
INSERT INTO #{TABLE_NAME}
|
|
121
|
+
(worker_name, action, from_workers, to_workers, reason,
|
|
122
|
+
queue_depth, latency_seconds, metrics_json, dry_run, created_at)
|
|
123
|
+
VALUES
|
|
124
|
+
(#{conn.quote('_debug_test')},
|
|
125
|
+
#{conn.quote('skipped')},
|
|
126
|
+
#{conn.quote(0)},
|
|
127
|
+
#{conn.quote(0)},
|
|
128
|
+
#{conn.quote('debug test - should be deleted')},
|
|
129
|
+
#{conn.quote(0)},
|
|
130
|
+
#{conn.quote(0.0)},
|
|
131
|
+
#{conn.quote(nil)},
|
|
132
|
+
#{conn.quote(true)},
|
|
133
|
+
#{conn.quote(Time.current)})
|
|
134
|
+
RETURNING id
|
|
135
|
+
SQL
|
|
136
|
+
|
|
137
|
+
result = conn.execute(test_sql)
|
|
138
|
+
test_id = result.first&.fetch('id', nil)
|
|
139
|
+
|
|
140
|
+
if test_id
|
|
141
|
+
diagnostics[:can_insert] = true
|
|
142
|
+
# Clean up test record
|
|
143
|
+
conn.execute("DELETE FROM #{TABLE_NAME} WHERE id = #{test_id}")
|
|
144
|
+
else
|
|
145
|
+
diagnostics[:issues] << 'INSERT succeeded but no id returned'
|
|
146
|
+
end
|
|
147
|
+
rescue StandardError => e
|
|
148
|
+
diagnostics[:issues] << "Error inserting test record: #{e.message}"
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
else
|
|
152
|
+
diagnostics[:issues] << 'No database connection available'
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Summary
|
|
156
|
+
diagnostics[:would_save_events] = diagnostics[:record_events_effective] &&
|
|
157
|
+
diagnostics[:table_exists] &&
|
|
158
|
+
diagnostics[:can_insert]
|
|
159
|
+
|
|
160
|
+
diagnostics
|
|
161
|
+
rescue StandardError => e
|
|
162
|
+
{
|
|
163
|
+
timestamp: Time.current,
|
|
164
|
+
error: "#{e.class}: #{e.message}",
|
|
165
|
+
issues: ["Unexpected error during diagnostics: #{e.message}"]
|
|
166
|
+
}
|
|
167
|
+
end
|
|
168
|
+
|
|
51
169
|
# Creates a new scale event record.
|
|
52
170
|
# @param attrs [Hash] Event attributes
|
|
53
171
|
# @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
|
|
@@ -209,6 +327,71 @@ module SolidQueueAutoscaler
|
|
|
209
327
|
0
|
|
210
328
|
end
|
|
211
329
|
|
|
330
|
+
# Returns diagnostic information to help debug event storage issues.
|
|
331
|
+
# Use this method in Rails console to understand why events might not be stored.
|
|
332
|
+
#
|
|
333
|
+
# @example Check why events aren't being stored
|
|
334
|
+
# SolidQueueAutoscaler::ScaleEvent.diagnostics
|
|
335
|
+
# # => { table_exists: false, error: "Table does not exist..." }
|
|
336
|
+
#
|
|
337
|
+
# @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
|
|
338
|
+
# @return [Hash] Diagnostic information
|
|
339
|
+
def diagnostics(connection: nil)
|
|
340
|
+
conn = connection || default_connection
|
|
341
|
+
result = {
|
|
342
|
+
table_name: TABLE_NAME,
|
|
343
|
+
connection_class: conn.class.name,
|
|
344
|
+
table_exists: false,
|
|
345
|
+
event_count: 0,
|
|
346
|
+
recent_events: 0,
|
|
347
|
+
last_event_at: nil,
|
|
348
|
+
error: nil
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
# Check table existence
|
|
352
|
+
begin
|
|
353
|
+
result[:table_exists] = conn.table_exists?(TABLE_NAME)
|
|
354
|
+
rescue StandardError => e
|
|
355
|
+
result[:error] = "Failed to check table existence: #{e.message}"
|
|
356
|
+
return result
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
unless result[:table_exists]
|
|
360
|
+
result[:error] = "Table does not exist. Run: rails generate solid_queue_autoscaler:migration && rails db:migrate"
|
|
361
|
+
return result
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
# Get event counts
|
|
365
|
+
begin
|
|
366
|
+
result[:event_count] = count(connection: conn)
|
|
367
|
+
result[:recent_events] = count(since: 24.hours.ago, connection: conn)
|
|
368
|
+
|
|
369
|
+
# Get last event time
|
|
370
|
+
sql = "SELECT MAX(created_at) FROM #{TABLE_NAME}"
|
|
371
|
+
last_at = conn.select_value(sql)
|
|
372
|
+
result[:last_event_at] = last_at ? parse_time(last_at) : nil
|
|
373
|
+
rescue StandardError => e
|
|
374
|
+
result[:error] = "Failed to query events: #{e.message}"
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
# Add configuration info
|
|
378
|
+
begin
|
|
379
|
+
config = SolidQueueAutoscaler.config
|
|
380
|
+
result[:config] = {
|
|
381
|
+
record_events: config.record_events,
|
|
382
|
+
record_all_events: config.record_all_events,
|
|
383
|
+
record_events_effective: config.record_events?,
|
|
384
|
+
connection_available: config.connection_available?
|
|
385
|
+
}
|
|
386
|
+
rescue StandardError => e
|
|
387
|
+
result[:config_error] = e.message
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
result
|
|
391
|
+
rescue StandardError => e
|
|
392
|
+
{ error: "Diagnostics failed: #{e.message}" }
|
|
393
|
+
end
|
|
394
|
+
|
|
212
395
|
private
|
|
213
396
|
|
|
214
397
|
def default_connection
|