Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Solid Queue can be used with SQL databases such as MySQL, PostgreSQL, or SQLite,
- [Threads, processes, and signals](#threads-processes-and-signals)
- [Database configuration](#database-configuration)
- [Other configuration settings](#other-configuration-settings)
- [Validating the configuration](#validating-the-configuration)
- [Lifecycle hooks](#lifecycle-hooks)
- [Errors when enqueuing](#errors-when-enqueuing)
- [Concurrency controls](#concurrency-controls)
Expand Down Expand Up @@ -408,6 +409,22 @@ There are several settings that control how Solid Queue works that you can set a
- `clear_finished_jobs_after`: period to keep finished jobs around, in case `preserve_finished_jobs` is true — defaults to 1 day. When installing Solid Queue, [a recurring job](#recurring-tasks) is automatically configured to clear finished jobs every hour on the 12th minute in batches. You can edit the `recurring.yml` configuration to change this as you see fit.
- `default_concurrency_control_period`: the value to be used as the default for the `duration` parameter in [concurrency controls](#concurrency-controls). It defaults to 3 minutes.

### Validating the configuration

You can validate the Solid Queue configuration ahead of time, without starting any process. This is handy in deploy scripts or CI to catch mistakes—a typo in `recurring.yml`, no processes configured, and so on—before they cause a supervisor to boot into a broken state:

```bash
# Using the bin/jobs binstub
bin/jobs check

# Or via rake
bin/rails solid_queue:check
```

Both commands validate the configuration for the current Rails environment. On success they print `Solid Queue configuration is valid.` and exit `0`; otherwise they print the errors and exit non-zero. When the number of threads is larger than the [database connection pool](#database-configuration), they also print an advisory warning about it—the same one the supervisor logs on boot. They're tolerant of a missing database connection, so they can run on CI or deploy hosts without database credentials.

`bin/jobs check` accepts the same options as `bin/jobs start` (e.g. `--config_file`, `--recurring_schedule_file`, `--skip-recurring`). The rake task honors the same environment variables Solid Queue already uses: `SOLID_QUEUE_CONFIG`, `SOLID_QUEUE_RECURRING_SCHEDULE`, and `SOLID_QUEUE_SKIP_RECURRING`. To validate a specific environment's configuration, set `RAILS_ENV`, for example `RAILS_ENV=production bin/jobs check`.


## Lifecycle hooks

Expand Down
6 changes: 6 additions & 0 deletions lib/solid_queue/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,11 @@ def self.exit_on_failure?
def start
SolidQueue::Supervisor.start(**options.symbolize_keys)
end

desc :check, "Validates the Solid Queue configuration for the current Rails env without starting anything. Exits non-zero on errors."
def check
configuration = SolidQueue::Configuration.new(**options.symbolize_keys)
exit 1 unless configuration.check
end
end
end
32 changes: 28 additions & 4 deletions lib/solid_queue/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -68,16 +68,40 @@ def standalone?
mode.fork? || @options[:standalone]
end

def warn_about_undersized_thread_pool
if (db_pool_size = SolidQueue::Record.connection_pool&.size) && db_pool_size < estimated_number_of_threads
SolidQueue.logger.warn "Solid Queue is configured to use #{estimated_number_of_threads} threads but the " +
"database connection pool is #{db_pool_size}. Increase it in `config/database.yml`"
def warnings
@warnings ||= [ undersized_thread_pool_message ].compact
end

def check
warnings.each { |warning| $stdout.puts "Warning: #{warning}" }

if valid?
$stdout.puts "Solid Queue configuration is valid."
true
else
$stderr.puts "Invalid Solid Queue configuration:"
errors.full_messages.each do |message|
message.each_line { |line| $stderr.puts " #{line.chomp}" }
end
false
end
end

private
attr_reader :options

def undersized_thread_pool_message
db_pool_size = SolidQueue::Record.connection_pool&.size

if db_pool_size && db_pool_size < estimated_number_of_threads
"Solid Queue is configured to use #{estimated_number_of_threads} threads but the " \
"database connection pool is #{db_pool_size}. Increase it in `config/database.yml`"
end
rescue ActiveRecord::ActiveRecordError
# No usable database connection. Skip the pool-size warning in that case.
nil
end

def ensure_configured_processes
unless configured_processes.any?
errors.add(:base, "No processes configured")
Expand Down
2 changes: 1 addition & 1 deletion lib/solid_queue/supervisor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def start(**options)
configuration = Configuration.new(**options)

if configuration.valid?
configuration.warn_about_undersized_thread_pool
configuration.warnings.each { |warning| SolidQueue.logger.warn(warning) }

klass = configuration.mode.fork? ? ForkSupervisor : AsyncSupervisor
klass.new(configuration).tap(&:start)
Expand Down
5 changes: 5 additions & 0 deletions lib/solid_queue/tasks.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,9 @@
task start: :environment do
SolidQueue::Supervisor.start
end

desc "validate the Solid Queue configuration for the current Rails env without starting any process"
task check: :environment do
exit 1 unless SolidQueue::Configuration.new.check
end
end
35 changes: 35 additions & 0 deletions test/unit/cli_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,46 @@ class CliTest < ActiveSupport::TestCase
end
end

test "check exits 0 and prints OK message for a valid configuration" do
out, err = capture_io do
assert_nothing_raised { SolidQueue::Cli.start([ "check", "--skip-recurring" ]) }
end

assert_match "Solid Queue configuration is valid.", out
assert_empty err
end

test "check exits 1 and prints invalid recurring task errors" do
out, err, exit_status = capture_check_run(
"--recurring_schedule_file", config_file_path(:recurring_with_invalid).to_s
)

assert_equal 1, exit_status
assert_match "Invalid Solid Queue configuration", err
assert_match "periodic_invalid_class", err
assert_match "periodic_incorrect_schedule", err
assert_empty out
end

private
def configuration_from_cli(**cli_options)
cli = SolidQueue::Cli.new([], cli_options)
options = cli.options.symbolize_keys.compact

SolidQueue::Configuration.new(**options)
end

# capture_io re-raises SystemExit before returning the captured strings, so we
# swallow it inside the block and return its status alongside the captured IO.
def capture_check_run(*args)
status = nil
out, err = capture_io do
begin
SolidQueue::Cli.start([ "check", *args ])
rescue SystemExit => e
status = e.status
end
end
[ out, err, status ]
end
end
48 changes: 36 additions & 12 deletions test/unit/configuration_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -170,24 +170,48 @@ class ConfigurationTest < ActiveSupport::TestCase
assert configuration.valid?
end

test "warns on boot when the database pool is smaller than the thread pool" do
log = StringIO.new
with_solid_queue_logger(ActiveSupport::Logger.new(log)) do
configuration = SolidQueue::Configuration.new(workers: [ { queues: "background", threads: 50, polling_interval: 10 } ])
configuration.warn_about_undersized_thread_pool
test "reports an undersized thread pool as a warning rather than an error" do
configuration = SolidQueue::Configuration.new(workers: [ { queues: "background", threads: 50, polling_interval: 10 } ], skip_recurring: true)

assert configuration.valid?
assert_equal 1, configuration.warnings.size
assert_match /Solid Queue is configured to use \d+ threads but the database connection pool is \d+\. Increase it in `config\/database.yml`/, configuration.warnings.first
end

test "has no warnings when the database connection pool is large enough" do
configuration = SolidQueue::Configuration.new(workers: [ { queues: "background", threads: 1, polling_interval: 10 } ], skip_recurring: true)

assert_empty configuration.warnings
end

test "check prints a success message and returns true for a valid configuration" do
out, err = capture_io do
assert SolidQueue::Configuration.new(skip_recurring: true).check
end

assert_match /Solid Queue is configured to use \d+ threads but the database connection pool is \d+\. Increase it in `config\/database.yml`/, log.string
assert_match "Solid Queue configuration is valid.", out
assert_empty err
end

private
def with_solid_queue_logger(logger)
old_logger, SolidQueue.logger = SolidQueue.logger, logger
yield
ensure
SolidQueue.logger = old_logger
test "check prints warnings to stdout on the valid path" do
out, _err = capture_io do
assert SolidQueue::Configuration.new(workers: [ { queues: "background", threads: 50, polling_interval: 10 } ], skip_recurring: true).check
end

assert_match "Solid Queue configuration is valid.", out
assert_match /Warning: Solid Queue is configured to use \d+ threads but the database connection pool is \d+/, out
end

test "check prints errors to stderr and returns false for an invalid configuration" do
out, err = capture_io do
assert_not SolidQueue::Configuration.new(recurring_schedule_file: config_file_path(:recurring_with_invalid)).check
end

assert_match "Invalid Solid Queue configuration:", err
assert_match "periodic_invalid_class", err
end

private
def assert_processes(configuration, kind, count, **attributes)
processes = configuration.configured_processes.select { |p| p.kind == kind }
assert_equal count, processes.size
Expand Down
50 changes: 50 additions & 0 deletions test/unit/rake_tasks_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# frozen_string_literal: true

require "test_helper"
require "rake"

class RakeTasksTest < ActiveSupport::TestCase
setup do
@previous_rake_application = Rake.application
@rake = Rake::Application.new
Rake.application = @rake
Rake::Task.define_task(:environment)
load File.expand_path("../../lib/solid_queue/tasks.rb", __dir__)
end

teardown do
Rake.application = @previous_rake_application
end

test "solid_queue:check exits 0 and prints OK message for a valid configuration" do
SolidQueue::Configuration.any_instance.stubs(:skip_recurring_tasks?).returns(true)

out, err = capture_io do
assert_nothing_raised { @rake["solid_queue:check"].invoke }
end

assert_match "Solid Queue configuration is valid.", out
assert_empty err
end

test "solid_queue:check exits 1 and prints errors for an invalid configuration" do
SolidQueue::Configuration.any_instance.stubs(:invalid_tasks).returns(
[ stub(key: "broken", errors: stub(full_messages: [ "is invalid" ])) ]
)
SolidQueue::Configuration.any_instance.stubs(:skip_recurring_tasks?).returns(false)

status = nil
out, err = capture_io do
begin
@rake["solid_queue:check"].invoke
rescue SystemExit => e
status = e.status
end
end

assert_equal 1, status
assert_empty out
assert_match "Invalid Solid Queue configuration:", err
assert_match "broken", err
end
end
Loading