Que Documentation

repository·master·Indexed 25 days ago

https://github.com/que-rb/que

Que is a high-performance, reliable job queue for Ruby and PostgreSQL that uses advisory locks to provide ACID guarantees for background jobs. It supports priority-based worker pools, exponential backoff for retries, and integration with ActiveRecord, Sequel, and ActiveJob. The library includes a CLI for managing workers and provides tools for handling job lifecycles, custom error notification, and database schema migrations.

Tokens
11.3K
Snippets
32
Records
64
Agent score
81%

What's inside Que

  1. Configure worker priorities and counts

    master

    Que uses a priority-based worker pool. You can manage capacity at different priority levels using --worker-priorities or --worker-count.

    • --worker-priorities [LIST]: Assigns specific priorities to workers. For example, --worker-priorities=10,30,50,any,any,any reserves one worker for priority < 10, one for < 30, one for < 50, and three for any priority.
    • --worker-count [COUNT]: A shorthand for creating N workers at the any priority level.
    • Combining both: If both are provided, --worker-count will pad or trim the list with any workers. For example, --worker-priorities=20,30,40 --worker-count=6 results in 20,30,40,any,any,any.
  2. Shut down workers safely with SIGKILL

    master

    Que attempts to block worker processes from exiting until jobs complete to avoid data corruption during transactions. However, if jobs are long-running, a normal exit might hang.

    If you need to force a shutdown, use SIGKILL. Unlike a normal exit (which uses Thread#kill), SIGKILL causes the Ruby process to end immediately. When the connection to PostgreSQL is lost, the database will roll back any open transactions and unlock the job, allowing it to be retried by another worker. This is the approach used by platforms like Heroku (SIGTERM, then SIGKILL after a timeout).

  3. Write reliable and idempotent jobs

    master

    Because jobs can be interrupted and retried, they should be designed to handle partial execution.

    Pattern 1: Transactional Database Updates

    For jobs that only interact with your database, wrap both the data changes and the destroy call in a single transaction. This ensures that either both succeed or both fail, preventing duplicate processing.

    class UpdateWidgetPrice < Que::Job
      def run(widget_id)
        widget = Widget[widget_id]
        price  = ExternalService.get_widget_price(widget_id)
    
        ActiveRecord::Base.transaction do
          widget.update price: price
          destroy
        end
      end
    end

    Pattern 2: Idempotent External Side Effects

    For jobs that interact with external APIs (like credit card charging), use idempotency keys or checks to ensure that retries do not cause duplicate side effects.

    class ChargeCreditCard < Que::Job
      def run(user_id, credit_card_id)
        # Check if the action was already performed before acting
        unless CreditCardService.check_for_previous_charge(credit_card_id)
          CreditCardService.charge(credit_card_id, amount: "$10.00")
        end
    
        ActiveRecord::Base.transaction do
          User.where(id: user_id).update_all charged_at: Time.now
          destroy
        end
      end
    end

    Pattern 3: Non-Transactional Jobs

    If a job cannot be wrapped in a transaction (e.g., sending an email), you can omit the destroy call. Que will detect that the job wasn't destroyed and clean it up automatically.

  4. How to test Que jobs

    master

    There are two primary ways to approach testing Que jobs:

    1. Synchronous execution: Set Que::Job.run_synchronously = true. This causes JobClass.enqueue to execute the job's logic immediately in the same process, as if you called JobClass.run(*args) directly.
    2. Asynchronous assertion: Leave run_synchronously disabled and assert on the state of the job records stored in the database after enqueuing.
    Que::Job.run_synchronously = true
  5. Run specs without Docker

    master
    To run specs locally without Docker, you must have a Postgres instance running. You can provide the connection details via the DATABASE_URL environment variable. If you only want to run the database via Docker Compose, use docker compose up -d db.
  6. Migrate the Que database schema

    master

    Que requires database schema updates for certain releases. Use Que.migrate!(version: X) to upgrade or downgrade. To remove Que entirely, migrate to version 0.

    In an ActiveRecord migration:

    class UpdateQue < ActiveRecord::Migration[5.0]
      def self.up
        Que.migrate!(version: 3)
      end
    
      def self.down
        Que.migrate!(version: 2)
      end
    end

    Manually in a console:

    Que.migrate!(version: 3)
    Que.db_version # => 3
  7. Use multiple queues

    master

    Que supports multiple queues within a single job table. This is useful for scenarios where different codebases share the same queue.

    Running workers for a specific queue: Use the --queue-name flag or the -q flag. You can specify multiple queues.

    Enqueuing jobs to a specific queue:

    1. At enqueue time: Pass job_options: { queue: 'name' } to the enqueue method.
    2. In the Job class: Set self.queue = 'name' within the job class definition.
    # Run workers for specific queues
    que --queue-name credit_cards
    # OR
    que -q default -q credit_cards
    # Enqueue with specific queue
    ProcessCreditCard.enqueue(current_user.id, job_options: { queue: 'credit_cards' })
    
    # Set default queue for a class
    class ProcessCreditCard < Que::Job
      self.queue = 'credit_cards'
    end
  8. Maintain the que_jobs table with manual vacuuming

    master

    Because the que_jobs table is high-churn, dead tuples can accumulate and slow down job acquisition. While PostgreSQL's autovacuum usually handles this, you may need to run a manual VACUUM if your database is under heavy load or has large tables that delay autovacuum processes.

    Example of a recurring manual vacuum job using Sequel:

    class ManualVacuumJob < CronJob
      self.priority = 1 # Highest priority to keep the table healthy
      INTERVAL = 300
    
      def run(args)
        DB.run "VACUUM VERBOSE ANALYZE que_jobs"
      end
    end
    class ManualVacuumJob < CronJob
      self.priority = 1 # set this to the highest priority since it keeps the table healthy for other jobs
      INTERVAL = 300
    
      def run(args)
        DB.run "VACUUM VERBOSE ANALYZE que_jobs"
      end
    end
  9. Prevent worker hangs with timeouts

    master

    Long-running jobs or hanging network requests can block workers and prevent graceful shutdowns. Always use timeouts on operations prone to hanging, such as HTTP requests.

    class ScrapeStuff < Que::Job
      def run(url_to_scrape)
        # Use the timeout feature of your HTTP library
        result = YourHTTPLibrary.get(url_to_scrape, timeout: 5)
    
        ActiveRecord::Base.transaction do
          # Insert result...
          destroy
        end
      end
    end
  10. Configure Que with Sequel

    master

    When using Sequel, assign the Sequel database instance directly to Que.connection.

    If you are using Sequel's migrator, ensure you require 'que' and set the connection within your migration blocks to ensure the schema updates correctly.

    DB = Sequel.connect(ENV['DATABASE_URL'])
    Que.connection = DB
    
    # In migrations:
    require 'que'
    Sequel.migration do
      up do
        Que.connection = self
        Que.migrate!(version: 7)
      end
      down do
        Que.connection = self
        Que.migrate!(version: 0)
      end
    end
  11. Setup Que with ActiveRecord without Rails

    master

    To use Que with ActiveRecord in a non-Rails application, establish your connection and then tell Que to use the ActiveRecord connection pool.

    ActiveRecord::Base.establish_connection(ENV['DATABASE_URL'])
    
    require 'que'
    Que.connection = ActiveRecord
  12. Configure plain Postgres connections without an ORM

    master

    If you are not using ActiveRecord or Sequel, you must manage your own Postgres connection pool. Que supports the ConnectionPool and Pond gems.

    Warning: If you are using ActiveRecord or Sequel, do not use these methods. Using separate connections is less efficient and prevents you from wrapping jobs in the same transactions as your data, which is critical for reliability.

    Using ConnectionPool

    Add gem 'connection_pool' to your Gemfile. Note that ConnectionPool maintains all connections in the pool even if they are idle.

    Using Pond

    Add gem 'pond' to your Gemfile. Pond is similar to ConnectionPool but establishes connections lazily, avoiding unnecessary overhead.

    require 'uri'
    require 'pg'
    require 'connection_pool'
    
    uri = URI.parse(ENV['DATABASE_URL'])
    
    Que.connection = ConnectionPool.new(size: 10) do
      PG::Connection.open(
        host:     uri.host,
        user:     uri.user,
        password: uri.password,
        port:     uri.port || 5432,
        dbname:   uri.path[1..-1]
      )end