joobq v0.5.6

JoobQ is a fast, efficient asynchronous reliable job queue and job scheduler library processing. Jobs are submitted to a job queue, where they reside until they are able to be scheduled to run in a computing environment.

JoobQ

JoobQ Logo

Crystal CI License: MIT

A Redis-backed asynchronous job queue system for Crystal

Features • Installation • Quick Start • Unified documentation • API Reference


Overview

Start with the unified JoobQ and JoobQ Pro documentation: released-version quick starts, Core concepts, Pro workflows, configuration, and recovery guides. Adding Pro explains repository access, the compatible dependency pair, and feature setup. The book source lives here; hosted GitBook sync must be configured separately.

For workflows, follow the typed DSL quick start, available since JoobQ Pro v0.2.2, then explore sequential and parallel flows. The authoring API remains a preview; existing constructors remain supported.

Explore task-based Guides, reusable Design Patterns, Production Deployment, and JoobQ Cloud for hosting on your cloud infrastructure.

Enterprise v1 remains under review. The opt-in leased runtime and migration guide describes the new delivery API, management authentication and compatibility changes. See validation and open release gates before deployment.

JoobQ is an asynchronous job queue system built for the Crystal programming language. Designed for both simplicity and scalability, JoobQ provides a robust framework for managing background job processing in applications of any size—from startup MVPs to large-scale distributed systems.

Why Choose JoobQ?

JoobQ combines the performance advantages of Crystal with the reliability of Redis to deliver a job queue system that doesn't compromise on speed, reliability, or developer experience.

Performance

  • Batched Redis operations; measure sustained completion throughput for your workload
  • Built on Crystal's efficient concurrency model
  • Optimized Redis operations for minimal latency

Reliability

  • Automatic retry mechanisms with exponential backoff
  • Dead letter queue for failed job analysis
  • Job expiration and timeout handling
  • Graceful draining and best-effort requeue; jobs must tolerate redelivery

Developer Experience

  • Clean, intuitive API following Crystal idioms
  • Comprehensive OpenAPI 3.0 specification
  • Type-safe job definitions
  • Minimal configuration required

Flexibility

  • Priority-based queue processing
  • Cron-like recurring job scheduling
  • Dynamic throttling and rate limiting
  • Middleware support for cross-cutting concerns

Key Features

  • 🚀 Measured Performance: Benchmark completed work with your payloads, TLS and durability settings; see current evidence and open release gates
  • ⚡ Priority Queues: Configure worker allocation based on queue priority
  • 🔄 Retry Logic: Built-in exponential backoff and customizable retry strategies
  • ⏰ Scheduling: Cron-style recurring jobs and delayed execution
  • 🎯 Throttling: Fine-grained rate limiting to prevent system overload
  • 📊 Monitoring: Comprehensive REST API for metrics and job management
  • 🔌 OpenAPI Integration: Complete API specification for client generation
  • 🛡️ Error Tracking: Detailed error monitoring and reporting
  • ♻️ Dead Letter Queue: Automatic handling of failed jobs
  • 🔒 Type Safety: Compile-time guarantees for job definitions

Table of Contents

Getting Started

Prerequisites

Before you begin, ensure you have the following installed:

  • Crystal (version 0.34.0 or higher)
  • Redis (version 5.0 or higher recommended)

Quick Installation

  1. Clone the repository (for development):

    git clone https://github.com/azutoolkit/joobq.git
    cd joobq
    
  2. Install dependencies:

    shards install
    
  3. Start Redis (if not already running):

    redis-server
    

Installation

Add the following to your shard.yml:

dependencies:
  joobq:
    github: azutoolkit/joobq

Then run:

shards install

Usage

require "joobq"

Environment Variables

REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_POOL_SIZE=500
REDIS_POOL_TIMEOUT=5.0
REDIS_PASS=somepass
REDIS_URL=rediss://user:password@redis.example.com:6380/0 # see "Connecting with a URL"
REDIS_TIMEOUT=0.2
JOOBQ_EVENTS_MAX_BYTES=67108864
JOOBQ_PAYLOAD_KEYS=2026a:<64 hex digits>,2025:<64 hex digits> # see "Encrypting Job Arguments"

JOOBQ_EVENTS_MAX_BYTES is the most memory the joobq:events stream may use (0 turns the limit off). The stream is also capped at about 50 000 entries. The byte limit is enforced from the worker heartbeat (every 10 s, at most one process checks per 10 s), so it is approximate: the stream can overshoot between beats, and it is trimmed to 90 % of the limit, never below 100 entries.

Connecting with a URL

REDIS_URL (or redis.url in the YAML file) names the server in one value: redis://[user:password@]host[:port][/database], rediss:// for TLS, or unix:///path/to/redis.sock[?db=N]. A user name selects a Redis 6+ ACL user (AUTH user password; the redis shard alone only authenticates the default user), a password alone is the default user's, and the path is the database. Percent-encode @ / : ? # % inside the user name or password; a literal + stays a +. A query or fragment is refused (a credential put there would otherwise be ignored), except db on a unix URL.

REDIS_URL is used as it is, by the YAML loader and by the default store alike: REDIS_HOST, REDIS_PORT and REDIS_PASS do not change it, and it wins over redis.url. A URL that cannot be used stops start-up with a message that does not repeat it; JoobQ never falls back to a plain connection to REDIS_HOST because of a bad URL.

For rediss:// the server certificate must be signed by a certificate authority JoobQ trusts (the system's, plus the PEM file in REDIS_CA_FILE / redis.ca_file if given: the file adds to the system's authorities, it does not replace them) and must be issued for the host in the URL. A certificate any trusted authority issued for another host is refused. REDIS_TLS_VERIFY=false (redis.tls_verify: false) turns both checks off; any value other than true/false/1/0/yes/no/on/off is an error. SSLv3 and TLS 1.0/1.1 are refused. The redis shard sends no server name (SNI), so a server that chooses its certificate by name needs a default one. For a store made from a URL the password never appears in logs, error messages or inspect/pp, and a connection that fails to authenticate is closed, never used as the default user. The host/port constructor uses the same credential-safe client and timeout policy. When several YAML files are merged, the redis section of the last one that has one wins. A host written with a trailing dot or an IPv6 literal in brackets is accepted.

In code: JoobQ::RedisStore.new(url: "rediss://...", ca_file: "ca.pem"); store.close ends current connections for either constructor; later requests reconnect.

Configuration

JoobQ can be configured using the JoobQ.configure method. Here is an example configuration:

JoobQ.configure do
  queue "default", 10, EmailJob

  scheduler do
    cron("*/1 * * * *") { # Do Something }
    every 1.hour, EmailJob, email_address: "john.doe@example.com"
  end
end

Configuration Properties

The JoobQ::Configure struct provides several properties that can be customized to configure the JoobQ system:

  • default_queue: The name of the default queue. Defaults to "default".
  • retries: The number of retries for a job. Defaults to 3.
  • expires: The expiration time for a job. Defaults to 3.days.
  • timeout: The maximum execution time allowed for a job. Defaults to 2.seconds.
  • failed_ttl: The time-to-live for failed jobs. Defaults to 3.milliseconds.
  • dead_letter_ttl: The time-to-live for jobs in the dead letter queue. Defaults to 7.days.
  • job_registry: An instance of JobSchemaRegistry for managing job schemas.
  • store: The store instance used for job storage and retrieval. Defaults to RedisStore.

Example Configuration with All Properties

JoobQ.configure do |config|
  config.default_queue = "default"
  config.retries = 5
  config.expires = 2.days
  config.timeout = 5.seconds
  config.failed_ttl = 1.minute
  config.dead_letter_ttl = 14.days

  queue "default", 10, EmailJob
  queue "priority", 5, PriorityJob, throttle: { limit: 100, period: 1.minute }

  scheduler do
    cron("*/1 * * * *") { # Do Something }
    every 1.hour, EmailJob, email_address: "john.doe@example.com"
  end
end

Shutting Down

Changed: JoobQ.forge now traps SIGTERM, SIGINT and SIGHUP by default, replacing any handler your application installed for them earlier (use trap_signals: false to keep yours).

JoobQ.forge traps SIGTERM, SIGINT and SIGHUP (Process.on_terminate covers all three): it calls JoobQ.shutdown and then exits the process. With the trap installed forge never returns, and code after the call does not run.

Exit status Meaning
0 clean shutdown: every job finished
3 the deadline passed with jobs still running; they were returned to their queues
1 a second signal, more than a second after the first, ended the shutdown early

Pass trap_signals: false (or set JOOBQ_TRAP_SIGNALS=0) when your application installs its own handlers and calls JoobQ.shutdown itself; forge then returns once the shutdown has finished.

JoobQ.forge(trap_signals: false, shutdown_timeout: 20.seconds)
# elsewhere, in your own handler:
JoobQ.shutdown(20.seconds)

JoobQ.shutdown(timeout = JoobQ::SHUTDOWN_TIMEOUT) (15 seconds) stops, in order and within timeout in total: the REST API started by forge, the schedulers (cron, recurring, delayed, the reaper) and the workers of every queue. It then waits for the jobs that are running, and at the deadline (one second before timeout, which is kept for what follows) it puts every job that is still running, and every job that was claimed but had not started, back at the end of its queue, one atomic step per job, and flushes the latency counters. JoobQ.abandoned_jobs says how many it had to put back. It is safe to call more than once and from any fiber: the first call does the work and returns true, later calls (also while the first runs) wait for it and return false. After a shutdown the queues of the process cannot be started again.

A job that is put back keeps executing in the old process until it ends (a fiber cannot be killed), and may finish its own side effects inside perform. What JoobQ does for that execution is switched off: it is not retried, dead-lettered, counted, announced or removed from the processing list, so the requeued copy is the only one JoobQ schedules. Workers claim nothing once the shutdown has begun.

Jobs put back run again from the start, possibly in another process while this one winds down, so they must be safe to run twice, as they must be after a crash. A process that is killed (SIGKILL) cannot do this; its jobs stay in the queue's processing list. Start-up recovery (Queue#start) moves at most one job per queue from that list back to the queue, and it takes jobs of any process serving the queue: a process that starts while another one is working on the same queue can move that process's running job back, so it runs twice. Prefer a graceful stop (SIGTERM) in rolling deploys.

Defining Queues

Queues are of type Hash(String, Queue(T)) where the name of the key matches the name of the Queue.

Example:

JoobQ.configure do
  queue name: "single", workers: 10, job: Job1, throttle: { limit: 20, period: 1.minute }
  queue "example", 10, ExampleJob | FailJob

  # Scheduling Recurring Jobs
  scheduler do
    cron("*/1 * * * *") { # Do Something }
    cron("*/5 20-23 * * *") { # Do Something }
    every 1.hour, ExampleJob, x: 1
  end
end

Queue Throttling

The worker throttle mechanism in JoobQ works in conjunction with the Queue Throttle Limit property to manage the rate at which jobs are processed. Here's how it works:

Queue Throttle Limit Property

The Queue Throttle limit property sets a maximum number of jobs that can be processed within a specific time frame. This helps to control the load on the system and ensures that the job processing rate does not exceed a certain threshold.

How They Work Together

  • Job Availability and Throttle Limit: The worker checks the queue for available jobs. If the number of jobs processed within the specified time frame reaches the throttle limit, the worker will wait until it is allowed to process more jobs. This prevents the system from being overwhelmed by too many jobs at once.

  • Job Expiration: Before processing a job, the worker checks if the job has expired. If the job has expired, it is marked as expired and not processed. This ensures that only valid jobs are processed, reducing unnecessary work.

  • Controlled Shutdown: The worker can be stopped gracefully by sending a termination signal. This allows for a controlled shutdown, ensuring that no jobs are abruptly terminated.

Example:

Here is an example of how you might configure the Queue Throttle limit property:

JoobQ.configure do
  queue "default", 10, EmailJob, throttle: { limit: 100, period: 60.seconds }
end

In this example, the throttle limit is set to 100 jobs per 60 seconds. This means that the worker will process up to 100 jobs every 60 seconds. If the limit is reached, the worker will wait until the next period to continue processing jobs.

Summary

The worker throttle mechanism, combined with the Queue Throttle limit property, ensures that job processing is controlled and efficient. By managing job availability, expiration, and processing rate, JoobQ provides a robust system for handling job queues without overwhelming the system.

Defining Jobs

To define Jobs, include the JoobQ::Job module, and implement the perform method.

struct EmailJob
  include JoobQ::Job

  # Name of the queue to be processed by
  @queue   = "default"
  # Number Of Retries for this job
  @retries = 0
  # Job Expiration
  @expires = 1.days.total_seconds.to_i

  # Initialize as normal with or without named tuple arguments
  def initialize(@email_address : String)
  end

  def perform
    # Logic to handle job execution
  end
end

Executing Job:

# Enqueue the job (Async)
EmailJob.enqueue(email_address: "john.doe@example.com")

# Batch enqueue jobs
EmailJob.batch_enqueue([job1, job2, job3])

# Perform Immediately
EmailJob.new(email_address: "john.doe@example.com").perform

# Delayed
EmailJob.delay(for: 1.hour, email_address: "john.doe@example.com")
EmailJob.enqueue_at(time: 3.minutes.from_now, email_address: "john.doe@example.com")

# Recurring at given interval
EmailJob.schedule(every: 1.second, email_address: "john.doe@example.com")

Best Practices for Defining Jobs

When defining jobs in JoobQ, it's important to follow certain best practices to ensure reliability and maintainability. Here are some key recommendations:

Idempotency

Jobs must be idempotent. This means that running the same job multiple times should produce the same result. Idempotency is crucial for ensuring that jobs can be retried safely without causing unintended side effects. To achieve idempotency:

  • Avoid modifying external state directly within the job.
  • Use unique identifiers to track job execution and prevent duplicate processing.
  • Ensure that any side effects (e.g., database updates, API calls) are safe to repeat.
Simple Primitive Types for Arguments

Job arguments must be simple primitive types such as integers, strings, and booleans. This ensures that the job data can be easily serialized and deserialized, and reduces the risk of errors during job execution. Complex objects or data structures should be avoided as job arguments.

Number of Arguments

Keep the number of arguments for jobs to a minimum. Having too many arguments can make the job definition complex and harder to maintain. As a best practice:

  • Limit the number of arguments to 3-5.
  • Group related arguments into a single object if necessary.
  • Use default values for optional arguments to simplify job invocation.

By following these best practices, you can ensure that your jobs are reliable, maintainable, and easy to work with in the JoobQ framework.

Encrypting Job Arguments

A job is stored as JSON in Redis (queue, processing list, retry and scheduled sets, dead letters), readable by anyone with access to Redis or a backup. Declare the sensitive properties with encrypted_property and they are written as AES-256-GCM sealed strings and opened again when the worker loads the job, before perform runs. Jobs that do not use it pay nothing.

class ChargeJob
  include JoobQ::Job

  property order_id : Int64 = 0 # stays readable
  encrypted_property card_token : String = ""
  encrypted_property billing : Hash(String, String)? = nil

  def perform
    charge(card_token) # plain text here
  end
end

Keys: one is current and seals; older ones only open. A key is id:key (32 bytes as 64 hex digits or base64; the id is 1 to 16 of a-z 0-9 _ -, default when omitted).

JoobQ.configure do
  payload_encryption current: "2026a:#{ENV["NEW_KEY"]}", previous: ["2025:#{ENV["OLD_KEY"]}"]
end

Without that call the keys come from JOOBQ_PAYLOAD_KEYS (comma separated id:key, first is current) or JOOBQ_PAYLOAD_KEY (one key). To rotate, put the new key first and keep the old one until no job sealed with it can still be in Redis; a retried job is rewritten with the current key.

  • Each value carries its key id, a random 96 bit nonce and the tag. The job class and the property name are authenticated data, so a value copied to another field or class is refused. Renaming the class or the property makes stored values unreadable; pass job_name: to encrypted_property to pin the name.
  • A job whose values cannot be opened because of the job (unknown key id, wrong class or field, altered, damaged) is never run: it goes to the dead-letter set as the job itself with error type decryption_error and a message that names the class and property but holds no value. Once the key is back, redrive it (dead-letter API or DeadLetterQueue#redrive) and it runs.
  • A problem with the process's keys (none configured, JOOBQ_PAYLOAD_KEYS malformed) is a configuration error: the queue of a job class with encrypted properties refuses to start, enqueuing raises JoobQ::Encryption::Error, and a worker that meets such a job puts it back on its queue (atomically, not counted as processed) and stops claiming for a few seconds (Encryption.key_backoff, monotonic clock) instead of dead-lettering it.
  • Values written before the property was encrypted are read as they are and sealed on the next write. Set JoobQ.config.allow_plaintext_payloads = false to make workers refuse them (the job is dead-lettered without its payload, which holds the plain values: that legacy job is gone, so drain old jobs first rather than flipping the flag). The same withholding applies to any other parse error in a job class with encrypted properties (Queue#add and the HTTP API still accept plain values, which are then sealed). While it is true, anyone who can write to Redis can substitute an unsealed value and it will run.
  • The library scrubs the plain values (and their to_s, inspect, JSON forms and the leaves of hashes and arrays; strings under 4 characters and numbers under 6 digits excepted, booleans never) from the error messages, causes and backtraces it records (error context, job.failed event, stored job error). It cannot scrub what your own perform logs. Scrubbing never raises: if it cannot be done the text is replaced by [REDACTED: not scrubbable], so the job still retries or dead-letters.
  • The sealed value is bound to the class and property, not to the job id: someone who can write to Redis can copy a sealed value between jobs of the same class and property. Nonces are random 96 bit values: rotate keys before about 2^32 seals per key.
  • Sealing is not deterministic (random nonce per value): two serialisations of one job differ, so nothing may re-serialise a job to find it again by value.
  • Properties are sealed only in the stored payload: in memory, and in your own logs, they are plain. String values are sealed as they are, other types as their JSON text. The size of a value is not hidden.

JoobQ HTTP Server

The APIServer class provides a REST API to interact with the JoobQ job queue system. It listens for HTTP requests and handles job enqueuing, job registry retrieval, and queue metrics.

To start the API server.

APIServer.start

Rest API

JoobQ provides a REST API to interact with the job queue. Below are the available endpoints:

GET /joobq/jobs/registry

This endpoint returns all available registered jobs and their JSON schemas that can be enqueued via the REST API.

Request:

GET /joobq/jobs/registry HTTP/1.1
Host: localhost:8080

Response:

{
  "EmailJob": {
    "type": "object",
    "properties": {
      "email_address": {
        "type": "string"
      }
    },
    "required": ["email_address"]
  }
}

POST /joobq/jobs

This endpoint allows users to enqueue jobs.

Request:

POST /joobq/jobs HTTP/1.1
Host: localhost:8080
Content-Type: application/json
Content-Length: 175

{
    "jid": "a13324f4-bdd8-4cf5-b566-c0c9c312f68b",
    "queue": "queue:test",
    "retries": 3,
    "expires": {{timestamp_plus_30}},
    "status": "enqueued",
    // Job initialization arguments
    "x": 10
}

Response:

{
  "status": "Job enqueued",
  "queue": "default",
  "job_id": "some-unique-job-id"
}

REST API & OpenAPI Specification

JoobQ provides a comprehensive REST API for job management, monitoring, and debugging. The API is fully documented with an OpenAPI 3.0 specification that enables easy client generation and integration.

API Endpoints

  • Job Management: Enqueue jobs, retrieve job registry
  • Queue Operations: Reprocess busy jobs, monitor queue health
  • Error Monitoring: Track errors, get statistics, filter by type/queue
  • Health Checks: System status and monitoring

OpenAPI Specification

The complete OpenAPI 3.0 specification is available in the docs/ directory:

  • Specification: docs/openapi.yaml
  • Documentation: docs/API_SPECIFICATION.md
  • Client Examples: examples/crystal_client_example.cr

Quick Start with the API

# Health check
curl http://localhost:8080/joobq/health/check

# Enqueue a job
curl -X POST http://localhost:8080/joobq/jobs \
  -H "Content-Type: application/json" \
  -d '{
    "queue": "email",
    "job_type": "EmailJob",
    "data": {
      "email": "user@example.com",
      "subject": "Welcome"
    }
  }'

# Get error statistics
curl http://localhost:8080/joobq/errors/stats

# Reprocess busy jobs
curl -X POST http://localhost:8080/joobq/queues/default/reprocess

Client Generation

Generate clients for your preferred language:

# Install OpenAPI Generator
brew install openapi-generator

# Generate clients
openapi-generator generate -i docs/openapi.yaml -g crystal -o clients/crystal/
openapi-generator generate -i docs/openapi.yaml -g typescript-axios -o clients/typescript/
openapi-generator generate -i docs/openapi.yaml -g python -o clients/python/

Interactive Documentation

View the interactive API documentation:

docker run -p 8080:8080 -e SWAGGER_JSON=/docs/openapi.yaml -v $(pwd)/docs:/docs swaggerapi/swagger-ui

Performance

The historical 35,000 jobs/second claim is not a v1 acceptance result. This audit did not reproduce a durable, end-to-end benchmark or the earlier cross-language comparison. Measure successful completions, queue growth, p95/p99 latency and memory with your payloads, Redis persistence and telemetry enabled.

The opt-in leased runtime now supports bounded delivery batches. On the current test host, three independent Redis primaries with verified TLS and AOF always completed one million 1 KiB no-op jobs in 50.91 and 52.25 seconds, including fixture startup/cleanup and per-job receipt verification. See the reproduction commands, raw evidence and limits. These finite no-op runs do not establish sustained application throughput.

Further delivery allocation optimizations reduced measured GC allocations by 26.7% for single jobs and 32.4% for batches. The optimized three-partition, 1 KiB run confirmed one million completions in 43.98 seconds on the same host, with TLS and AOF always retained.

The Queue, Worker and Scheduler allocation pass reduced managed allocations by 32.6%, 32.9% and 35.4%, respectively, in separate 1 KiB admission, execution/settlement and delayed-promotion benchmarks. These optimize the existing list-based API; they do not enable Delivery leases.

The subsequent core/Pro performance review adds journal allocation improvements and fixes failed Redis transaction connection reuse, with Pro reconciliation and metrics optimizations in the paired branch.

See the production v1 audit and prioritized roadmap and current implementation status. Safe receipt retention, sustained soak and operational acceptance remain release gates.

Runtime limits and structured logs

worker_batch_size caps claimed/executing jobs per worker. A queue's normal capacity is approximately workers * worker_batch_size; this is not yet a global cap across replacement workers or detached application fibers.

REDIS_CONNECT_TIMEOUT and REDIS_COMMAND_TIMEOUT configure DNS/connect and socket command limits in seconds (default 5.0, positive). They are distinct from REDIS_POOL_TIMEOUT, and do not constitute an end-to-end request deadline. A command whose reply is lost raises JoobQ::RedisOutcomeUnknown instead of being replayed automatically. A later, explicit request reconnects. Resolve mutating requests by their application identity before retrying them.

store = JoobQ::RedisStore.new(connect_timeout: 2.seconds, command_timeout: 3.seconds)
Log.setup(:info, Log::IOBackend.new(STDOUT, formatter: JoobQ::JSONLogFormatter))

The JSON formatter preserves structured identifiers in data and application correlation fields in context. Redact sensitive values before logging. Durability depends on Redis persistence, replication and recovery configuration; external side effects need idempotency keys.

Contributing

  1. Fork it (https://github.com/azutoolkit/joobq/fork)
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create a new Pull Request

Testing

To run the tests, use the following command:

crystal spec

Deployment

To deploy JoobQ, ensure that you have a running Redis instance. You can use the provided docker-compose.yml to set up Redis.

docker-compose up -d

License

The MIT License (MIT). Please see License File for more information.

Acknowledgments

Elias J. Perez - creator and maintainer Crystal Language Redis

Enterprise hardening branch

The delivery/storage migration is tracked in the implementation status. The experimental delivery ledger provides explicit admission outcomes, claim tokens and index repair in a separate versioned namespace. Existing queues still use the legacy protocol; this is not a production cutover or an enterprise-v1 release declaration.

Run integration specs with python3 scripts/run_integration.py (or add --url). It starts isolated plain, second-store and verified TLS Redis fixtures and rejects skipped specs and runtime crashes. python3 scripts/redis_restart_smoke.py exercises an owned Redis SIGKILL/AOF restart. Both require Crystal, Redis with TLS support, OpenSSL and Python 3. They never use an inherited Redis endpoint.

RedisPipeline#execute_batch takes Proc(Redis::PipelineApi, Nil) operations and returns the actual pipeline results. A batch uses one pooled connection; errors propagate and a failed pipeline's connection is discarded so unread replies cannot reach another borrower. Pipelines are not atomic: an error can follow partially applied commands. Resolve identities before retrying mutations. Legacy zero-argument batch closures are deprecated and execute independently.

Reviewed operator actions

For a UI or MCP client that separates review from execution, use the conditional APIs:

require "digest/sha256"
store = JoobQ.store.as(JoobQ::RedisStore)
dlq = JoobQ::DeadLetterQueue.new(store.redis)
entry = dlq.find(job_id) || raise "Job no longer in the DLQ"
reviewed_digest = Digest::SHA256.hexdigest(entry.member)
# Persist this digest with the operator's review, then execute later:
outcome = dlq.redrive_reviewed(job_id, reviewed_digest)

# Pause only if the queue is still unpaused at the Redis atomic update:
changed = store.set_paused_reviewed("default", expected: false, paused: true)

A changed DLQ member returns conflicted; a removed member returns missing. Successful replay atomically moves the exact reviewed member back to its queue. Replaying can repeat external effects, so handlers still need idempotency. Queue pause/resume returns false when the current pause flag differs from the reviewed flag; it does not stop jobs already running. These APIs enforce state preconditions, not operator authorization.

Automatic releases

Prepare a new shard.yml version in a reviewed pull request. After merge and the complete CI matrix, automation tags the exact tested commit and publishes a GitHub release. Unchanged versions do not create releases; CI never pushes a version commit to protected master. The tagged manifest and runtime VERSION stay in sync. See release automation for version policy, permissions, retry recovery and validation.

Repository

joobq

Owner
Statistic
  • 35
  • 2
  • 0
  • 0
  • 2
  • about 2 hours ago
  • June 20, 2020
License

GNU Lesser General Public License v2.1

Links
Synced at

Thu, 05 Nov 2026 13:03:10 GMT

Languages