Solid Queue Documentation

repository·main·Indexed 25 days ago

https://github.com/rails/solid_queue

A database-based queuing backend for Active Job designed for simplicity and performance using SQL databases such as MySQL, PostgreSQL, or SQLite. It requires Rails 7.1+ and Ruby 3.2+. The library features a supervisor that manages workers, dispatchers, and schedulers, supporting both fork and async modes, as well as fiber-based workers for I/O-bound jobs.

Tokens
9.5K
Snippets
25
Records
48
Agent score
71%

What's inside Solid Queue

  1. Choose between Fork and Async supervisor modes

    main

    The Supervisor Mode determines how the supervised actors (workers, dispatchers, etc.) are run relative to the supervisor process.

    The supervisor forks a separate process for each supervised actor. This provides the best isolation and performance but uses more memory.

    Async Mode

    All actors run in the same process as the supervisor using different threads. This is useful if forking is not supported or memory is extremely constrained.

    How to use Async mode:

    • Run via CLI: bin/jobs --mode async
    • Or set the environment variable: SOLID_QUEUE_SUPERVISOR_MODE=async

    Note: In async mode, the processes configuration option is ignored. This mode is separate from the worker's concurrency model (which uses threads or fibers).

    bin/jobs --mode async
  2. Concurrency limits for scheduled jobs

    main

    For jobs scheduled in the future (using Active Job's wait or wait_until), concurrency limits are enforced when the jobs are due, not when they are initially scheduled.

    Example flow:

    1. Multiple jobs are scheduled with wait: 10.minutes.
    2. At the 10-minute mark, they are enqueued.
    3. If the concurrency limit is 1, the first job runs and the second is blocked.
    4. Once the first job finishes, the second is unblocked and becomes ready.

    Warning: Mixing concurrency controls with future scheduling can impact performance because jobs must be enqueued one-by-one rather than in batches to ensure limits are respected.

  3. Understand Solid Queue actors: Workers, Dispatchers, Scheduler, and Supervisor

    main

    Solid Queue uses several specialized actors to manage job lifecycles:

    • Workers: Pick up ready jobs from queues and process them (using the solid_queue_ready_executions table).
    • Dispatchers: Select scheduled jobs that are due and move them from solid_queue_scheduled_executions to solid_queue_ready_executions. They also handle concurrency control maintenance.
    • Scheduler: Manages recurring tasks by enqueuing them when they are due.
    • Supervisor: The orchestrator that runs workers, dispatchers, and schedulers according to your configuration, manages their heartbeats, and handles starting/stopping them.
  4. Handle job enqueuing and transactional integrity

    main

    Because Solid Queue can reside in the same database as your application, you can leverage ACID transactions to ensure jobs are only enqueued if the surrounding database transaction commits. However, relying on this behavior can cause issues if you later move to a different job backend or a separate database.

    Defer enqueuing until transaction commit

    To avoid relying on database-level transactional integrity, you can use the Active Job feature enqueue_after_transaction_commit. This defers the job enqueuing until the transaction successfully commits. This can be enabled globally in ApplicationJob or for specific jobs.

    Best practices for avoiding transactional coupling

    If you do not use enqueue_after_transaction_commit, ensure you:

    1. Enqueue jobs using after_commit callbacks.
    2. Or, configure Solid Queue to use a different database connection than your main application.

    Example of configuring a separate database for Solid Queue:

    # In your environment configuration
    config.solid_queue.connects_to = { database: { writing: :primary, reading: :replica } }
    class ApplicationJob < ActiveJob::Base
      self.enqueue_after_transaction_commit = true
    end
  5. How concurrency semaphores and unblocking work

    main

    Solid Queue uses semaphores to manage concurrency.

    1. Enqueuing: When a job is enqueued, Solid Queue checks the semaphore for the computed key. If the semaphore is open, the job is marked as ready and can be picked up by workers. If closed, the job is either blocked or discarded based on on_conflict.
    2. Execution: When a job finishes (successfully or unsuccessfully), it signals the semaphore.
    3. Unblocking: Signaling the semaphore triggers an attempt to unblock the next job with the same key. Unblocking moves a job from blocked to ready; it does not guarantee immediate execution.
    4. Failsafe: The duration parameter acts as a failsafe. If a job fails to release its semaphore (e.g., due to a machine crash), jobs blocked longer than duration become candidates for release.

    Important Notes:

    • Order: Jobs are unblocked based on priority, but queue order is not taken into account for unblocking. Once unblocked, workers pick them up following their own configured queue order.
    • Retries: Failed jobs that are retried are treated like new jobs; they must acquire an open semaphore before running, regardless of previous attempts.
  6. How queue order and priorities work

    main

    Solid Queue uses two mechanisms for ordering jobs:

    1. Queue Order: If a worker is configured with a list of queues (e.g., [real_time, background]), it polls them in that specific order. No jobs will be taken from background unless real_time is empty.
    2. Job Priority: Active Job supports integer priorities. In Solid Queue, smaller values have higher priority. Within a single queue, jobs are picked by priority.

    Warning: Queue order takes precedence over job priority. If real_time has low-priority jobs and background has high-priority jobs, the worker will still pick the real_time jobs first. It is recommended to use either queue order or priorities, but not both mixed together.

  7. Configure Fiber Workers and Isolation

    main

    Fiber workers execute jobs as fibers on a single reactor thread. This is best for cooperative, I/O-bound jobs.

    Requirements & Best Practices:

    • Isolation: You MUST set config.active_support.isolation_level = :fiber in your Rails application. Solid Queue will refuse to boot fiber workers if isolation remains thread-scoped.
    • Database Connections: On Rails 7.2+, you can often use a smaller pool (e.g., 3-5 connections per worker process) because Active Record can release connections during non-blocking waits. On Rails 7.1, size the pool more conservatively.
    • Avoid Blocking: CPU-heavy or blocking code will block the entire fiber reactor thread.
  8. Manage failed jobs and retries

    main

    Solid Queue does not implement an automatic retry mechanism; it relies on Active Job for retries. When a job fails, a record is created in the solid_queue_failed_executions table. These failed executions persist until they are manually discarded or re-enqueued via the Rails console.

    To manage failed jobs:

    1. Find the execution using SolidQueue::FailedExecution.find(...).
    2. Inspect the error using .error.
    3. Use .retry to re-enqueue the job as if it were new.
    4. Use .discard to delete the job from the system.
    failed_execution = SolidQueue::FailedExecution.find(...) # Find the failed execution related to your job
    failed_execution.error # inspect the error
    
    failed_execution.retry # This will re-enqueue the job as if it was enqueued for the first time
    failed_execution.discard # This will delete the job from the system
  9. Optimize polling performance

    main

    To ensure optimal performance and use covering indexes, follow these best practices:

    1. Use exact queue names instead of wildcards (e.g., queues: [background, backend] instead of queues: back*).
    2. Avoid paused queues. Pausing queues requires a DISTINCT query that can be slower on some databases.

    Wildcard prefixes (like beta*) require an extra step to fetch the list of matching queues before polling, which can impact performance depending on the database engine.

  10. Configure Solid Queue with a single database

    main

    If you prefer not to use a separate database for the queue, you can use a single database for both the app and the queue:

    1. Copy the contents of db/queue_schema.rb into a standard Rails migration and delete db/queue_schema.rb.
    2. Remove config.solid_queue.connects_to from your production configuration.
    3. Run your migrations.

    In this mode, database.yml only needs a primary entry.

  11. Upgrade to Solid Queue 1.x: enqueue_after_transaction_commit? change

    main
    In version 1.x, the value returned for enqueue_after_transaction_commit? has changed to true and is no longer configurable via Solid Queue settings. To modify this behavior, you must use Active Job's configuration options.
  12. Use the Puma plugin for Solid Queue

    main

    The Puma plugin allows you to run the Solid Queue supervisor alongside Puma, letting Puma manage the supervisor processes.

    Configuration

    Add the plugin to your puma.rb:

    plugin :solid_queue

    To avoid running Solid Queue in development environments where it might not be configured, wrap the plugin in a conditional check using an environment variable:

    plugin :solid_queue if ENV["SOLID_QUEUE_IN_PUMA"]

    Execution Modes

    • fork (Default/Recommended): The plugin forks additional processes for each worker and dispatcher. This provides the best isolation and performance but uses more memory.
    • async: Workers and dispatchers run within the same Puma process(s). This is useful if you have specific reasons to avoid forking, but the processes configuration option is ignored in this mode.

    Note: Phased restarts are not supported because the plugin requires app preloading.

    plugin :solid_queue
    # To run in async mode instead of the default fork mode:
    solid_queue_mode :async