module Divider

divider

A PostgreSQL-backed job queue for Quarz, on rotor. Jobs are rows of divider_jobs, inserted in your transactions and run by workers in Quarz tasks: Oban's and Solid Queue's shape (no Redis). Named after the frequency divider of a quartz movement, which turns the crystal's oscillation into the pulses that drive the hands.

# Cog.toml of an application
[dependencies]
Divider = { git = "https://github.com/quarz-lang/divider", branch = "main" }

The API reference is at https://divider.api.quarz.dev.

The design is divider#1, its comments the decisions taken while it was built.

The schema

Divider::Migrations.all are rotor migrations (divider_jobs, and divider_cron for the cron entries): add them to your migrator's list.

def migrate(repo: Rotor::Repo, app: Array[Type[Rotor::Migration]]) -> Void
  Rotor::Migrator.new(repo, app + Divider::Migrations.all).migrate
end

Jobs

data WelcomeArgs = (user_id: Int, template: String = "welcome")

class SendWelcome < Divider::Job
  queue :mail                    # default :default
  max_attempts 10                # default 20
  timeout 2.minutes              # an attempt longer than that fails
  job_args WelcomeArgs           # the arguments: a data type of the class's own
  unique by: [:user_id], period: 1.hour

  def perform(args: WelcomeArgs, job: Divider::Context) -> Void
    # job.repo: a repository over the worker's pool
  end
end

class Cleanup < Divider::Job
  def perform(job: Divider::Context) -> Void
    job.repo.exec("DELETE FROM sessions WHERE expires_at < now()")
  end
end

def signup(repo: Rotor::Repo) -> Void
  repo.transaction do |tx|
    # ... insert the user ...
    SendWelcome.enqueue(repo: tx, user_id: 42)               # exists once tx commits
    nil
  end
  SendWelcome.enqueue(repo: repo, user_id: 42, after: 10.minutes, priority: 1)
  SendWelcome.enqueue_args(repo: repo, args: WelcomeArgs.new(42, "again"))
  job = Cleanup.enqueue(repo, at: Time.utc(2026, 12, 1))     # Divider::Enqueued(id, state, scheduled_at)
  Divider.cancel(repo, job.id)                               # :cancelled, :requested (running) or nil
end

Running jobs

worker = Divider::Worker.new(ENV["DATABASE_URL"], jobs: [SendWelcome, Cleanup],
                             queues: {default: 10, mail: 5})
Signal.trap(:TERM) { worker.shutdown(25.seconds) }
worker.run      # or Task.spawn { worker.run } beside an HTTP server
# Divider.serve(url, jobs: ..., queues: ...): the same as a process's main (TERM/INT).

Recurring jobs

cron = Divider::Cron.new([Divider::Cron.entry("*/5 * * * *", Cleanup),
                          Divider::Cron.entry("0 3 * * *", SendWelcome, user_id: 1, template: "digest")])
Divider::Worker.new(url, jobs: [Cleanup, SendWelcome], queues: {default: 10, mail: 5}, cron: cron).run

Five-field expressions in UTC (@hourly, @daily and the like too). A worker checks every 15 seconds, by the database's clock, and each entry is enqueued once per due minute however many workers check (a worker enqueues entries whose classes it does not run as well: another worker of their queue runs them). A minute no worker checked (every worker stopped) is not made up afterwards. An entry's arguments are keywords, encoded when the entry is made and checked when the job is decoded.

Testing

class SignupTest < Test::Case
  include Divider::Testing::Assertions

  def test_welcome_is_enqueued
    Divider::Testing.fake do
      signup(repo)
      assert_enqueued(SendWelcome, user_id: 42)   # the arguments given, as JSON
    end
  end

  def test_cleanup_runs
    Divider::Testing.inline(repo) { Cleanup.enqueue(repo) }   # run as it is enqueued
    Divider::Testing.drain(repo, jobs: [Cleanup])             # or: run what is due, once
  end
end

In a bezel application

License

MIT No Attribution (LICENSE).

Functions

def cancel(repo: Rotor::Repo, id: Int) -> Symbol?

Cancels the job id: one scheduled, available or failed (a retry pending) becomes cancelled (:cancelled); a running one is asked to stop (:requested: its cancelled_at is set, which the job sees as cancel_requested?; it ends completed if it finishes anyway, cancelled otherwise, not retried; asked again, :requested again, the first request's time kept). Nil for a job that does not exist or has ended.

/home/runner/work/divider/divider/src/job.qz:260

def serve(url: String, *, jobs: Array[Type[Divider::Job]], queues: Hash[Symbol, Int] = …, grace: Duration = …, node: String? = …, poll: Duration = …, on_error: shareable (Exception, Divider::Context) -> Void? = …, on_event: shareable (Divider::Event) -> Void? = …, cron: Divider::Cron? = …) -> Void

A worker for a process of its own: runs Worker.new(url, ...) until SIGTERM or SIGINT, which shut it down with grace for the running jobs.

# src/main.qz of a worker process
Divider.serve(ENV["DATABASE_URL"], jobs: [Cleanup, PurgeSessions], queues: {default: 10, mail: 5})

/home/runner/work/divider/divider/src/worker.qz:510

Classes and modules