NYC
skills/dchuk/rails_ai_agents/solid-queue-setup

solid-queue-setup

SKILL.md

Solid Queue Setup for Rails 8

Overview

Solid Queue is Rails 8's default Active Job backend:

  • Database-backed (no Redis required)
  • Built-in concurrency controls
  • Supports priorities and multiple queues
  • Web UI available via Mission Control

Quick Start

bundle add solid_queue
bin/rails solid_queue:install
bin/rails db:migrate

Configuration

# config/solid_queue.yml
default: &default
  dispatchers:
    - polling_interval: 1
      batch_size: 500
  workers:
    - queues: "*"
      threads: 3
      processes: 1
      polling_interval: 0.1

development:
  <<: *default

production:
  <<: *default
  workers:
    - queues: [critical, default]
      threads: 5
      processes: 2
    - queues: [low]
      threads: 2
      processes: 1

Set as Active Job Adapter

# config/application.rb
config.active_job.queue_adapter = :solid_queue

Naming Convention

Use _later for async, _now for synchronous:

# Async (queued via Solid Queue) - preferred
SendWelcomeEmailJob.perform_later(user.id)

# Synchronous (runs immediately, skips queue) - use sparingly
SendWelcomeEmailJob.perform_now(user.id)

Creating Jobs

Basic Job

# app/jobs/send_welcome_email_job.rb
class SendWelcomeEmailJob < ApplicationJob
  queue_as :default

  def perform(user_id)
    user = User.find(user_id)
    UserMailer.welcome(user).deliver_now
  end
end

Job with Retries

# app/jobs/process_payment_job.rb
class ProcessPaymentJob < ApplicationJob
  queue_as :critical

  retry_on PaymentGatewayError, wait: :polynomially_longer, attempts: 5
  discard_on ActiveRecord::RecordNotFound

  rescue_from(StandardError) do |exception|
    ErrorNotifier.notify(exception)
    raise
  end

  def perform(order_id)
    order = Order.find(order_id)
    PaymentService.new.charge(order)
  end
end

Job with Priority

class UrgentNotificationJob < ApplicationJob
  queue_as :critical

  # Lower number = higher priority (default is 0)
  def priority
    -10
  end

  def perform(notification_id)
    notification = Notification.find(notification_id)
    notification.deliver!
  end
end

Enqueueing Jobs

# Enqueue immediately
SendWelcomeEmailJob.perform_later(user.id)

# Enqueue with delay
SendReminderJob.set(wait: 1.hour).perform_later(user.id)

# Enqueue at specific time
SendReportJob.set(wait_until: Date.tomorrow.noon).perform_later

# Enqueue on specific queue
ProcessJob.set(queue: :low).perform_later(data)

Recurring Jobs

# config/recurring.yml
production:
  daily_report:
    class: GenerateDailyReportJob
    schedule: every day at 6am
    queue: low

  cleanup:
    class: CleanupOldRecordsJob
    schedule: every sunday at 2am

  sync:
    class: SyncExternalDataJob
    schedule: every 15 minutes

  session_cleanup:
    class: SessionCleanupJob
    schedule: every day at 3am

Testing Jobs

Job Test Template

# test/jobs/send_welcome_email_job_test.rb
require "test_helper"

class SendWelcomeEmailJobTest < ActiveJob::TestCase
  setup do
    @user = users(:one)
  end

  test "sends welcome email" do
    assert_enqueued_emails 1 do
      SendWelcomeEmailJob.perform_now(@user.id)
    end
  end

  test "enqueues on default queue" do
    assert_enqueued_with(job: SendWelcomeEmailJob, queue: "default") do
      SendWelcomeEmailJob.perform_later(@user.id)
    end
  end
end

Testing Enqueueing

# test/jobs/process_payment_job_test.rb
require "test_helper"

class ProcessPaymentJobTest < ActiveJob::TestCase
  test "enqueues the job with correct arguments" do
    order = orders(:one)

    assert_enqueued_with(job: ProcessPaymentJob, args: [order.id]) do
      ProcessPaymentJob.perform_later(order.id)
    end
  end

  test "enqueues on critical queue" do
    assert_enqueued_with(job: ProcessPaymentJob, queue: "critical") do
      ProcessPaymentJob.perform_later(orders(:one).id)
    end
  end
end

Testing Job Side Effects

# test/jobs/cleanup_old_records_job_test.rb
require "test_helper"

class CleanupOldRecordsJobTest < ActiveJob::TestCase
  test "deletes old sessions" do
    old_session = sessions(:old)
    old_session.update!(created_at: 31.days.ago)
    recent_session = sessions(:one)

    CleanupOldRecordsJob.perform_now

    assert_not Session.exists?(old_session.id)
    assert Session.exists?(recent_session.id)
  end
end

Testing with perform_enqueued_jobs

# test/integration/signup_flow_test.rb
require "test_helper"

class SignupFlowTest < ActionDispatch::IntegrationTest
  test "signup sends welcome email" do
    perform_enqueued_jobs do
      post signups_path, params: {
        signup: { email: "new@example.com", name: "Test" }
      }
    end

    assert_emails 1
  end
end

Running Solid Queue

# Development
bin/rails solid_queue:start

# Production (Procfile)
web: bin/rails server
worker: bin/rails solid_queue:start

Monitoring

Mission Control (Web UI)

# Gemfile
gem "mission_control-jobs"

# config/routes.rb
mount MissionControl::Jobs::Engine, at: "/jobs"

Console Queries

SolidQueue::Job.where(finished_at: nil).count        # Pending
SolidQueue::FailedExecution.count                      # Failed
SolidQueue::FailedExecution.last.retry                 # Retry
SolidQueue::Job.where("finished_at < ?", 1.week.ago).delete_all  # Cleanup

Migration from Sidekiq

Sidekiq Solid Queue
perform_async(args) perform_later(args)
perform_in(5.minutes, args) set(wait: 5.minutes).perform_later(args)
sidekiq_options queue: 'critical' queue_as :critical
sidekiq_retry_in retry_on with wait:

Checklist

  • Solid Queue gem installed
  • Migrations run
  • Queue adapter configured
  • Jobs use perform_later (not perform_now)
  • Error handling with retry_on / discard_on
  • Recurring jobs configured
  • Job tests written
  • Mission Control mounted (optional)
  • All tests GREEN
Weekly Installs
2
First Seen
7 days ago
Installed on
opencode2
gemini-cli2
antigravity2
claude-code2
windsurf2
codex2