Project

zizq

0.0
The project is in a healthy, maintained state
This is the official Ruby client for the Zizq job queue server. Zizq is a simple, single binary, zero dependency, language agnostic job queue. Features: - Enqueue and process jobs across programming languages - Persistent/journalled - Multi-thread and/or multi-fiber - Scheduled jobs - Prioritized queues - Optional ActiveJob integration - Unique jobs - Cron scheduling (recurring jobs) - Job introspection and management, including `jq` filters This client supports multi-threaded and/or multi-fiber concurrency and is very fast. The Zizq server provides everything needed. There are no separate external storage dependencies to configure such as Redis or a RDBMS. See https://zizq.io for full details and documentation.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Runtime

~> 0.82
~> 1.7
 Project Readme

Zizq — Official Ruby Client

Zizq (/zɪsk/) is a fast and durable job queue packed into a single native binary, built on an embedded LSM database — not on Redis, and not on your RDBMS. It works in any stack, crossing programming language boundaries.

This is the official Zizq client library for Ruby.

CI Gem Version

Features

  • Multi-thread and/or multi-fiber concurrent worker (via async)
  • Zizq::Job based job classes, Active Job support, or low-level/custom
  • Enqueue and process jobs from one language to another
  • Arbitrary named queues
  • Granular job priorities
  • Scheduled jobs
  • Configurable backoff policies
  • Configurable job retention policies
  • Recurring jobs (cron)
  • Job introspection and management APIs, with support for jq query filters
  • Concurrency control that doesn't impact other jobs
  • Rate limiting with configurable burst
  • Unique jobs (deduplicated)
  • Batched jobs (folded/merged)
  • Testing helpers

Installation

Note

If you have not yet installed the Zizq server, follow the Getting Started guide first.

Add it to your application's Gemfile:

gem 'zizq', '~> 0.7.0'

Or install it manually:

$ gem install zizq -v 0.7.0

Ruby 3.2.8 or newer is required. Client and server share version numbers — keep the client's major/minor at or below the server's.

Configuration

Out of the box, the client talks to a server at http://localhost:7890 — fine for local development. For anything else, configure it with Zizq.configure in your application's bootstrap (e.g. a Rails initializer):

require 'zizq'

Zizq.configure do |c|
  c.url    = 'https://zizq.your.network:7890'
  c.logger = Logger.new('log/zizq.log')

  c.tls.ca = '/path/to/server-ca-cert.pem'

  # Optional worker defaults — applied to every Zizq::Worker
  # instance and to runs of the `zizq-worker` executable. Explicit
  # kwargs or CLI flags override these.
  c.worker.queues = ['emails', 'payments']
  c.worker.fiber_count  = 25
end

For mutual TLS, also set c.tls.client_cert and c.tls.client_key.

Caution

If your server is exposed directly to the internet, it should require mutual TLS — otherwise anybody can talk to it.

Usage

Tip

This README is an overview. The full documentation covers each feature in depth — middleware, custom dispatchers, Active Job, job querying, and more.

Defining a job

In most Ruby applications, a job is a plain class that includes Zizq::Job. The class declares its defaults with the zizq_* DSL and implements #perform:

class SendEmailJob
  include Zizq::Job

  zizq_queue 'emails'
  zizq_priority 100
  zizq_retry_limit 5

  def perform(user_id, template:)
    user = User.find(user_id)
    Mailer.deliver(user, template)
  end
end

Every default — zizq_queue, zizq_priority, zizq_retry_limit, zizq_backoff, zizq_retention, zizq_unique — can be overridden per enqueue. The job's class name ("SendEmailJob") becomes the API-level job type, so keep it stable once jobs are in flight.

Enqueuing jobs

Enqueue a job by passing the class and the arguments your #perform method expects:

job = Zizq.enqueue(SendEmailJob, 42, template: 'welcome')
job.id  # => "03fu0wm75gxgmfyfplwvazhex"

Override defaults for a single call with Zizq.enqueue_with, or with a block that mutates the request:

# Don't retry this one.
Zizq.enqueue_with(retry_limit: 0).enqueue(SendEmailJob, 42, template: 'welcome')

# Bump the priority via the block form.
Zizq.enqueue(SendEmailJob, 42, template: 'welcome') do |req|
  req.priority = 1000
end

Schedule a job for later with delay (seconds from now) or an absolute ready_at:

Zizq.enqueue_with(delay: 3600).enqueue(SendEmailJob, 42, template: 'welcome')
Zizq.enqueue_with(ready_at: Time.new(2027, 3, 15, 14, 30)).enqueue(SendEmailJob, 42, template: 'welcome')

To enqueue many jobs efficiently, Zizq.enqueue_bulk sends them in a single atomic request — across queues and job types, and job classes vs raw enqueues can be mixed in too:

Zizq.enqueue_bulk do |b|
  signups.each { |user_id| b.enqueue(SendEmailJob, user_id, template: 'welcome') }
end

Jobs can also be enqueued without Zizq::Job by providing the named fields — designed for lower-level code style, and for cross-language workflows where, for example, a Ruby app enqueues jobs consumed by a Go service.

Zizq.enqueue(
  type: "send_email",
  queue: "comms",
  payload: { user_id: 42, template: "welcome" }
)

Concurrency control

The Zizq server supports constraining the number of in_flight set of jobs for specific job types by binding a named budget to those jobs.

# Define a named budget called "notifications" that jobs can reference
# (generally somewhere in your application startup code).
Zizq.define_budget(
  "notifications",
  allocation: 10,
  strategy: { type: :while_in_flight }
)

# Specify the budget on the Job class (or at enqueue time).
class SendNotificationJob
  include Zizq::Job

  zizq_budget "notifications"

  def perform(id)
    # ...
  end
end

# Those jobs execute no more than 10 at any given time. Other jobs continue to
# flow normally.
100.times do |n|
  Zizq.enqueue(SendNotificationJob, n)
end

Rate limiting

The Zizq server supports dispatching jobs to workers at a throttled rate by binding a named budget to those jobs. Jobs that exceed the limit do not block other work. The server is smart enough to "park" them until the moment they are ready to dispatch.

# Define a named budget called "image-service" that jobs can reference
# (generally somewhere in your application startup code).
Zizq.define_budget(
  "image-service",
  allocation: 1000,
  strategy: {
    type: :time_based,
    duration: 1.minute # or just 60 without ActiveSupport
  }
)

# Specify the budget on the Job class (or at enqueue time).
class NormalizeImageJob
  include Zizq::Job

  zizq_budget "image-service"

  def perform(id)
    # ...
  end
end

# Those jobs are delivered by the server at a rate of 1000/minute without
# blocking other jobs. Your workers remain free of such knowledge.
5000.times do |n|
  Zizq.enqueue(NormalizeImageJob, n)
end

Cross-language and low-level dispatch

When a Ruby app needs to process jobs enqueued by another language (or itself using raw enqueue form), Zizq::Router maps type strings to handler blocks operating on plain JSON payloads:

Zizq.configure do |c|
  c.dispatcher = Zizq::Router.new do
    route('send_email') do |payload|
      Mailer.deliver(payload['user_id'], payload['template'])
    end

    # Apps that mix the two styles can fall back to Zizq::Job
    # for anything not handled by an explicit route.
    fallback { |job| Zizq::Job.call(job) }
  end
end

See Custom Dispatchers for full details. Dispatchers in Zizq are just objects that implement #call with a single Zizq::Resources::Job argument, and Zizq::Router is just a dispatcher itself.

Running a worker

Jobs are processed by a worker, typically in a separate process. The simplest way is the zizq-worker executable bundled with the gem. Rails apps need no arguments — zizq-worker auto-detects config/environment.rb when run from the app's root:

$ bundle exec zizq-worker
I, [...] INFO -- : Zizq worker starting: 1 threads, 25 fibers, prefetch=50
I, [...] INFO -- : Connected. Listening for jobs.

For Sinatra or other apps, pass the entrypoint explicitly:

$ bundle exec zizq-worker app.rb

Worker defaults (queues, thread_count, fiber_count, prefetch) come from your Zizq.configure { |c| c.worker.* } block. CLI flags (--threads, --fibers, --queue, --all-queues, etc.) override the configured defaults when needed. Leave --fibers 1 if your application isn't fiber-safe — no Async context is loaded in that case. INT / TERM trigger a graceful shutdown (drains in-flight jobs up to --shutdown-deadline, default 30s).

For more control — for example running the worker in-process alongside a Rack app — construct Zizq::Worker directly:

require 'zizq'

# Picks up queues, fiber_count, etc. from Zizq.configure { |c| c.worker.* };
# any kwarg here overrides those defaults.
worker = Zizq::Worker.new(queues: ['emails', 'payments'])

Signal.trap('INT') { worker.stop }
worker.run  # blocks until the worker stops

#run blocks until the worker terminates; #stop drains in-flight jobs gracefully, #kill forces an immediate stop. On any unclean shutdown the server returns unfinished jobs to the queue — no job is lost.

Recurring jobs (cron)

Define a cron schedule in your application's startup code. Definitions are idempotent — every process can safely define the same schedule, and Zizq keeps the server in sync by adding, replacing, and removing entries as the definition changes. Cron requires a Pro license on the server.

Zizq.define_crontab('maintenance', timezone: 'Europe/London') do |cron|
  # Every 15 minutes.
  cron.define_entry('refresh_warehouse', '*/15 * * * *').enqueue(
    RefreshWarehouseJob, incremental: true
  )

  # 9am London time, every day.
  cron.define_entry('daily_digest', '0 9 * * *').enqueue(SendDailyDigestJob)
end

Once defined, schedules can be inspected and managed via Zizq.crontab('maintenance') — paused/resumed at the schedule level or per entry, and deleted entirely when no longer needed.

Testing

Set c.test_mode = true in your test helper and Zizq swaps the real client out for an in-memory Zizq::Test::Client that buffers enqueues instead of dispatching them. Tests can then assert on what was enqueued and drain the buffer through the configured dispatcher — no running server required.

# test/test_helper.rb (or spec/spec_helper.rb)
Zizq::Test.enable!

class ActiveSupport::TestCase
  setup { Zizq::Test.reset! }
end

# In a test
def test_signup_fans_out
  SignupService.new.run

  assert Zizq::Test.enqueued?(SendWelcomeEmailJob, user_id: 42)
  assert_equal 2, Zizq::Test.pending_jobs(only_queues: 'emails').size

  # Drain the buffer through Zizq.configuration.dequeue_middleware
  # (same path the real worker takes — registered middleware runs too).
  Zizq::Test.dispatch_enqueued_jobs
end

See Testing for full details.

Resources

Support & Feedback

If you need help using Zizq, create an issue on the zizq-ruby repo. Feedback is very welcome.

License

MIT — see LICENSE.