BigBinary is building a suite of products under neeto. We currently have around 22 products under development and all of the products are using Sidekiq. After the launch of Solid Queue, we decided to migrate NeetoForm from Sidekiq to Solid Queue.
Please note that Solid Queue currently doesn't support cron-style or recurring jobs. There is a PR open regarding this issue. We have only partially migrated to Solid Queue. For recurring jobs, we are still using Sidekiq. Once the PR is merged, we will migrate completely to Solid Queue.
Here is a step-by-step migration guide you can use to migrate your Rails application from Sidekiq to Solid Queue.
gem "solid_queue"
to your Rails application's Gemfile and run
bundle install
.bin/rails generate solid_queue:install
which copies the config file and
the required migrations.bin/rails db:migrate
.The installation step should have created a config/solid_queue.yml
file.
Uncomment the file and modify it as per your needs. Here is how the file looks
for our application.
default: &default
dispatchers:
- polling_interval: 1
batch_size: 500
workers:
- queues: "auth"
threads: 3
processes: 1
polling_interval: 0.1
- queues: "urgent"
threads: 3
processes: 1
polling_interval: 0.1
- queues: "low"
threads: 3
processes: 1
polling_interval: 2
- queues: "*"
threads: 3
processes: 1
polling_interval: 1
development:
<<: *default
staging:
<<: *default
heroku:
<<: *default
test:
<<: *default
production:
<<: *default
On your development machine, you can start Solid Queue by running the following command.
bundle exec rake solid_queue:start
This will start Solid Queue's supervisor process and will start processing any
enqueued jobs. The supervisor process forks
workers and dispatchers
according to the configuration provided in the config/solid_queue.yml
file.
The supervisor process also controls the heartbeats of workers and dispatchers,
and sends signals to stop and start them when needed.
Since we use foreman, we added the above command to our Procfile.
# Procfile
web: bundle exec puma -C config/puma.rb
worker: bundle exec sidekiq -C config/sidekiq.yml
solidqueueworker: bundle exec rake solid_queue:start
release: bundle exec rake db:migrate
You can set the Active Job queue adapter to :solid_queue
by adding the
following line in your application.rb
file.
# application.rb
config.active_job.queue_adapter = :solid_queue
The above change sets the queue adapter at the application level for all the
jobs. However, since we wanted to use Solid Queue for our regular jobs and
continue using Sidekiq for cron jobs, we didn't make the above change in
application.rb
.
Instead, we created a new base class which inherited from ApplicationJob
and
set the queue adapter to :solid_queue
inside that.
# sq_base_job.rb
class SqBaseJob < ApplicationJob
self.queue_adapter = :solid_queue
end
Then we made all the classes implementing regular jobs inherit from this new
class SqBaseJob
instead of ApplicationJob
.
# send_email_job.rb
- class SendEmailJob < ApplicationJob
+ class SendEmailJob < SqBaseJob
# ...
end
By making the above change, all our regular jobs got enqueued via Solid Queue instead of Sidekiq.
But, we realized later that emails were still being sent via Sidekiq. On
debugging and looking into Rails internals, we found that ActionMailer
uses
ActionMailer::MailDeliveryJob
for enqueuing or sending emails.
ActionMailer::MailDeliveryJob
inherits from ActiveJob::Base
rather than the
application's ApplicationJob
. So even if we set the queue_adapter in
application_job.rb
, it won't work. ActionMailer::MailDeliveryJob
fallbacks
to using the adapter defined in application.rb
or environment-specific
(production.rb / staging.rb / development.rb) config files. But we can't do that
because we still want to use Sidekiq for cron jobs.
To use Solid Queue for mailers, we needed to override the queue_adapter for
mailers. We can do that in application_mailer.rb
.
# application_mailer.rb
class ApplicationMailer < ActionMailer::Base
# ...
ActionMailer::MailDeliveryJob.queue_adapter = :solid_queue
end
This change is only until we use both Sidekiq and Solid Queue. Once cron style
jobs feature lands in Solid Queue, we can remove this override and set the
queue_adapter directly in application.rb
which will enforce the setting
globally.
For migrating from Sidekiq to Solid Queue, we had to make the following changes to the syntax for enqueuing a job.
.perform_async
with .perform_later
..perform_at
with .set(...).perform_later(...)
.- SendMailJob.perform_async
+ SendMailJob.perform_later
- SendMailJob.perform_at(1.minute.from_now)
+ SendMailJob.set(wait: 1.minute).perform_later
At some places we were storing the Job ID on a record, for querying the job's status or for cancelling the job. For such cases, we made the following change.
def disable_form_at_deadline
- job_id = DisableFormJob.perform_at(deadline, self.id)
- self.disable_job_id = job_id
+ job_id = DisableFormJob.set(wait_until: deadline).perform_later(self.id)
+ self.disable_job_id = job.job_id
end
def cancel_form_deadline
- Sidekiq::Status.cancel(self.disable_job_id)
+ SolidQueue::Job.find_by(active_job_id: self.disable_job_id).destroy!
self.disable_job_id = nil
end
Initially, we thought the
on_thread_error
configuration
provided by Solid Queue can be used for error handling. However, during the
development phase, we noticed that it wasn't capturing errors. We raised
an issue with Solid Queue
as we thought it was a bug.
Rosa Gutiérrez responded on the issue and clarified the following.
on_thread_error
wasn't intended for errors on the job itself, but rather errors in the thread that's executing the job, but around the job itself. For example, if you had an Active Record's thread pool too small for your number of threads and you got an error when trying to check out a new connection, on_thread_error would be called with that.For errors in the job itself, you could try to hook into Active Job's itself.
Based on the above information, we modified our SqBaseJob
base class to handle
the exceptions and report it to Honeybadger.
# sq_base_job.rb
class SqBaseJob < ApplicationJob
self.queue_adapter = :solid_queue
rescue_from(Exception) do |exception|
context = {
error_class: self.class.name,
args: self.arguments,
scheduled_at: self.scheduled_at,
job_id: self.job_id
}
Honeybadger.notify(exception, context:)
raise exception
end
end
Remember we mentioned that ActionMailer
doesn't inherit from ApplicationJob
.
So similarly, we would have to handle exceptions for Mailers separately.
# application_mailer.rb
class ApplicationMailer < ActionMailer::Base
# ...
ActionMailer::MailDeliveryJob.rescue_from(Exception) do |exception|
context = {
error_class: self.class.name,
args: self.arguments,
scheduled_at: self.scheduled_at,
job_id: self.job_id
}
Honeybadger.notify(exception, context:)
raise exception
end
end
For retries, unlike Sidekiq, Solid Queue doesn't include any automatic retry
mechanism, it
relies on Active Job for this.
We wanted our application to retry sending emails in case of any errors. So we
added the retry logic in the ApplicationMailer
.
# application_mailer.rb
class ApplicationMailer < ActionMailer::Base
# ...
ActionMailer::MailDeliveryJob.retry_on StandardError, attempts: 3
end
Note that, although the queue adapter configuration can be removed from
application_mailer.rb
once the entire application migrates to Solid Queue,
error handling and retry override cannot be removed because of the way
ActionMailer::MailDeliveryJob
inherits from ActiveJob::Base
rather than
application's ApplicationJob
.
Once all the above changes were done, it was obvious that a lot of tests were
failing. Apart from fixing the usual failures related to the syntax changes,
some of the tests were failing inconsistently. On debugging, we found that the
affected tests were all related to controllers, specifically tests inheriting
from ActionDispatch::IntegrationTest
.
We tried debugging and searched for solutions when we stumbled upon Ben Sheldon's comment on one of Good Job's issues. Ben points out that this is actually an issue in Rails where Rails sometimes inconsistently overrides ActiveJob's queue_adapter setting with TestAdapter. A PR is already open for the fix. Thankfully, Ben, in the same comment, also mentioned a workaround for it until the fix has been added to Rails.
We added the workaround in our test helper_methods.rb
and called the method in
each of our controller tests which were failing.
# test/support/helper_methods.rb
def ensure_consistent_test_adapter_is_used
# This is a hack mentioned here: https://github.com/bensheldon/good_job/issues/846#issuecomment-1432375562
# The actual issue is in Rails for which a PR is pending merge
# https://github.com/rails/rails/pull/48585
(ActiveJob::Base.descendants + [ActiveJob::Base]).each(&:disable_test_adapter)
end
# test/controllers/exports_controller_test.rb
class ExportsControllerTest < ActionDispatch::IntegrationTest
def setup
ensure_consistent_test_adapter_is_used
# ...
end
# ...
end
Basecamp has released mission_control-jobs which can be used to monitor background jobs. It is generic, so it can be used with any compatible ActiveJob adapter.
Add gem "mission_control-jobs"
to your Gemfile and run bundle install
.
Mount the mission control route in your routes.rb
file.
# routes.rb
Rails.application.routes.draw do
# ...
mount MissionControl::Jobs::Engine, at: "/jobs"
By default, mission control would try to load the adapter specified in your
application.rb
or individual environment-specific files. Currently, Sidekiq
isn't compatible with mission control, so you will face an error while loading
the dashboard at /jobs
. The fix is to explicitly specify solid_queue
to the
list of mission control adapters.
# application.rb
# ...
config.mission_control.jobs.adapters = [:solid_queue]
Now, visiting /jobs
on your site should load a dashboard where you can monitor
your Solid Queue jobs.
But that isn't enough. There is no authentication. For development environments
it is fine, but the /jobs
route would be exposed on production too. By
default, Mission Control's controllers will extend the host app's
ApplicationController
. If no authentication is enforced, /jobs
will be
available to everyone.
To implement some kind of authentication, we can specify a different controller as the base class for Mission Control's controllers and add the authentication there.
# application.rb
# ...
MissionControl::Jobs.base_controller_class = "MissionControlController"
# app/controllers/mission_control_controller.rb
class MissionControlController < ApplicationController
before_action :authenticate!, if: :restricted_env?
private
def authenticate!
authenticate_or_request_with_http_basic do |username, password|
username == "solidqueue" && password == Rails.application.secrets.mission_control_password
end
end
def restricted_env?
Rails.env.staging? || Rails.env.production?
end
end
Here, we have specified that MissionControlController
would be our base
controller for mission control related controllers. Then in
MissionControlController
we implemented basic authentication for staging and
production environments.
We haven't had any complaints so far. Solid Queue offers simplicity, requires no additional infrastructure and provides visibility for managing jobs since they are stored in the database.
In the coming days, we will migrate all of our 22 Neeto products to Solid Queue. And once cron style job support lands in Solid Queue, we will completely migrate from Sidekiq.
If this blog was helpful, check out our full blog archive.