Kaal
Distributed cron scheduling for plain Ruby.
kaal is the core engine gem. It owns scheduler/runtime behavior, the registry APIs, the plain Ruby CLI, delayed-job dispatch, and the optional SQL backend surfaces.
Installation
Use kaal by itself when you want the engine plus non-SQL coordination backends.
gem 'kaal'
Then install and initialize:
bundle install
bundle exec kaal init --backend=memory
kaal init creates:
config/kaal.ymlconfig/kaal-scheduler.yml
Supported backends:
memoryredis
If you want SQL persistence instead, add the runtime libraries your app uses, such as sequel, activerecord, sqlite3, pg, or mysql2, then set backend: sqlite/postgres/mysql plus backend_config in config/kaal.yml.
Configuration
Generated config/kaal.yml is the primary entrypoint:
defaults:
backend: memory
namespace: kaal
tick_interval: 5
window_lookback: 120
window_lookahead: 0
lease_ttl: 125
scheduler_config_path: config/kaal-scheduler.yml
enable_dispatch_recovery: true
enable_log_dispatch_registry: false
delayed_job_allowed_class_prefixes: []
backend_config: {}
Redis path:
defaults:
backend: redis
scheduler_config_path: config/kaal-scheduler.yml
backend_config:
url: redis://127.0.0.1:6379/0
Time zone behavior is explicit:
- use
config.time_zone = 'America/Toronto'when needed - otherwise scheduling runs in
UTC
Scheduler File
Default scheduler definitions live at config/kaal-scheduler.yml:
defaults:
jobs:
- key: "example:heartbeat"
cron: "*/5 * * * *"
job_class: "ExampleHeartbeatJob"
enabled: true
args:
- "{{fire_time.iso8601}}"
kwargs:
idempotency_key: "{{idempotency_key}}"
job_class must resolve to a Ruby constant that responds to one of:
.perform(*args, **kwargs).perform_later(*args, **kwargs).set(queue: ...).perform_later(*args, **kwargs)
For plain Ruby .perform jobs, Kaal treats the dispatch as successful unless the job raises an exception. Return values are ignored.
CLI
bundle exec kaal init --backend=memory
bundle exec kaal start
bundle exec kaal status
bundle exec kaal tick
bundle exec kaal explain "*/15 * * * *"
bundle exec kaal next "0 9 * * 1" --count 3
E2E Verification
bin/rspec-e2e memory
REDIS_URL=redis://127.0.0.1:6379/0 bin/rspec-e2e redis
Runtime API
Recurring jobs:
Kaal.register(
key: 'reports:daily',
cron: '0 9 * * *',
enqueue: ->(fire_time:, idempotency_key:) {
ReportsJob.perform(fire_time: fire_time, idempotency_key: idempotency_key)
}
)
Kaal.start!
Delayed jobs:
Kaal.enqueue_at(
at: Time.now.utc + 300,
job_class: "InvoiceReminderJob",
args: [123],
queue: "mailers",
job_id: "invoice-reminder:123"
)
Rules shared by the runtime surface:
- delayed jobs use
job_idas their identity and require it to be unique while pending - delayed-job
argsare positional only - recurring and delayed jobs share the same job-class dispatch rules
- string job classes are constantized and class or module values are used directly
To restrict delayed-job class names:
Kaal.configure do |config|
config.delayed_job_allowed_class_prefixes = ["Reports::", "Billing::"]
end
An empty delayed_job_allowed_class_prefixes list leaves delayed-job class resolution unrestricted. That is reasonable for local or trusted deployments. On shared Redis or SQL backends in production, set a restrictive prefix list.
SQL Backends
Use the explicit SQL backends when you want persisted registries:
Kaal::Backend::SQLiteKaal::Backend::PostgresKaal::Backend::MySQLkaal-railsfor Rails-native install and auto-wiring
For SQL-backed deployments, run the generated migrations so kaal_delayed_jobs exists alongside the recurring scheduler tables.
Postgres and supported MySQL versions claim due delayed jobs with SKIP LOCKED. Older SQL paths still preserve correctness with delete confirmation, and Kaal adds a small pre-claim jitter there to reduce multi-node contention.