active_job_cr
Version, currently master branch3 versions
- 0.1.1latestOct 19, 2026
- 0.1.0Oct 12, 2026
- master branchOct 12, 2026
github.com/OrelSokolov/active_job.cr
No description declared in shard.yml.
Installation
# Add this to your shard.yml
dependencies:
active_job_cr:
github: OrelSokolov/active_job.cr
branch: mastermaster is a branch, not a release, so this tracks it rather than pinning a version.
Then run:
shards installshard.yml
- Crystal
>= 1.21.0
Dependencies
Runtime Dependencies
- pg*github: will/crystal-pg
- redis*github: jgaskins/redis
- active_support_cr*path: ../active_support.cr
README
This README is the one indexed from the repository at its latest ref, not from the tag for this version.
active_job.cr
A Crystal port of Rails ActiveJob — background jobs with retries,
callbacks, serialization and pluggable queue adapters, plus a durable
PostgreSQL queue modeled on solid_queue 1.2.4 and a Redis queue.
Namespace ActiveJob mirrors the original 1:1. Estimated parity ~90%;
the single structural seam is job-class deserialization, solved with a
compile-time registry instead of Ruby's constantize.
⚠️ EXPERIMENTAL FRAMEWORK, DO NOT USE IN PRODUCTION YET.
Installation
dependencies:
active_job_cr:
github: OrelSokolov/active_job.cr
shards install
Requires Crystal >= 1.21.0; depends on active_support.cr,
active_record.cr (GlobalID) and crystal-pg for the SolidQueue adapter.
Usage
class ProcessJob < ActiveJob::Base
queue_as :default
retry_on MyError, wait: 3.seconds, attempts: 5, jitter: 0.15
discard_on ActiveJob::DeserializationError::RecordNotFound
def perform(user : User, priority : Int32)
# ...
end
end
ProcessJob.perform_later(user, 1)
ProcessJob.set(wait: 10.minutes, queue: :low).perform_later(user, 2)
Feature comparison vs ActiveJob (Rails 8.x)
DSL
| Feature | Original (activejob) | Port (active_job.cr) | Status |
|---|---|---|---|
class XJob < ActiveJob::Base, def perform(*args) | ✅ | Same | ✅ |
queue_as :name / block form; queue_with_priority N | ✅ | Same | ✅ |
retry_on Error, wait:, attempts:, jitter:, queue:, priority:, waits: :polynomially_longer (+ exhausted block) | ✅ | Same retry math (polynomial backoff executions**4 + rand*jitter + 2) | ✅ |
discard_on Error, after_discard | ✅ | Same | ✅ |
Callbacks: before/after/around_perform, before/after/around_enqueue | ✅ | Same | ✅ |
rescue_from | ✅ | Same | ✅ |
Continuable jobs (include ActiveJob::Continuable, step :name, start:, isolated:) | ✅ | Same (8.x feature) | ✅ |
Invocation & serialization
| Feature | Original | Port | Status |
|---|---|---|---|
perform_later (+ block), perform_now, set(queue:, wait:, wait_until:, priority:), perform_all_later (bulk) | ✅ | Same | ✅ |
Instance API: retry_job(wait:, queue:), job_id, queue_name, priority, arguments, executions, exception_executions, scheduled_at, provider_job_id, successfully_enqueued? | ✅ | Same | ✅ |
| Argument serialization: primitives as-is; Date/Time/Duration/Range/NamedTuple | ✅ | Via a serializer registry | ✅ |
GlobalID (gid://app/User/1), missing record → DeserializationError::RecordNotFound | ✅ | Compile-time registry of AR classes instead of constantize | ✅ |
Locale/timezone carried into the job (I18n.with_locale wrapper) | ✅ | Same | ✅ |
Adapters
| Feature | Original | Port | Status |
|---|---|---|---|
async adapter (default) | ✅ | Channel(Job) queue + fiber worker pool; scheduled jobs on a timer heap | ✅ |
inline, test adapters | ✅ | Same interface (enqueue/enqueue_at) | ✅ |
config.active_job.queue_adapter, per-class queue_adapter= | ✅ | Same | ✅ |
| solid_queue adapter | separate gem (8.x default) | ActiveJob::SolidQueue::Adapter: PG table solid_queue_jobs, FOR UPDATE SKIP LOCKED claims, priority DESC + FIFO by id, scheduled-job visibility, stale-claim reclaim (default 1 h) | 🔶 |
Recurring jobs (fugit cron semantics, @daily aliases, DOM/DOW union) | ✅ | ActiveJob::Cron + SolidQueue::RecurringTask with (task_key, run_at) dedup via partial unique index | 🔶 |
| Redis adapter | Sidekiq etc. | RedisQueueAdapter: LIST queue (LPUSH/RPOP), ZSET for scheduled, SET NX recurring dedup; ephemeral (no history, no in-queue priorities — by design) | 🔶 |
Test helpers (assert_enqueued_with, perform_enqueued_jobs) | ✅ | Not ported | ⬜ |
enqueue_after_transaction_commit | ✅ | Not ported | ⬜ |
Deviations from solid_queue (documented)
- One jobs table instead of the ready/claimed/scheduled split.
- No concurrency controls, pauses or process heartbeats.
- Recurring rows get
scheduled_at = now(the scheduler enqueues at tick time;run_atstores the occurrence).
Testing
crystal spec
Suite: 56/56. The SolidQueue oracle (oracle/solid_queue_cases.rb →
solid_queue_expected.yml, 15 cases) is recorded against the real
solid_queue 1.2.4 + fugit 1.12.1 and replayed unchanged in
spec/solid_queue_spec.cr; spec/redis_queue_spec.cr covers the Redis
queue contract. Benchmarks: bench/results/201-solid-queue.md.
Documentation
Built from the current release. The first visit to a release nobody has asked for starts its build.
Links
This branch
- Branch
master- Seen
- Oct 12, 2026
- Crystal
>= 1.21.0- Indexed
- yes
Dependents
Repository
github.com/OrelSokolov/active_job.cr
Metadata
- Created
- Oct 13, 2026
- Updated
- Oct 23, 2026
- Synced
- Oct 23, 2026
- Versions
- 3